From b5b9e64636f7e411c11bb6423d3525ca40e39d05 Mon Sep 17 00:00:00 2001 From: Lachezar Yankov Date: Sun, 5 Nov 2023 11:43:49 +0100 Subject: [PATCH 001/311] Moved the message about scalac -Yretain-trees (#1043) Now it appears in the "Automatic Derivation and case class default field values" as well --- docs/decoding.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/decoding.md b/docs/decoding.md index 8c9a1581c..e17cbfa42 100644 --- a/docs/decoding.md +++ b/docs/decoding.md @@ -51,6 +51,8 @@ implicit val decoder: JsonDecoder[Entity] = """{ "id": 42, "related": null }""".fromJson[Entity] ``` +_Note: If you’re using Scala 3 and your case class is defining default parameters, `-Yretain-trees` needs to be added to `scalacOptions`._ + ## ADTs Say we extend our data model to include more data types From 297c4f6c3ee275194eaeb1f7838c8a09e8aa30ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:11:27 +0100 Subject: [PATCH 002/311] Update README.md (#1044) Co-authored-by: github-actions[bot] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 070191c35..571bc9cf2 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The goal of this project is to create the best all-round JSON library for Scala: In order to use this library, we need to add the following line in our `build.sbt` file: ```scala -libraryDependencies += "dev.zio" %% "zio-json" % "0.6.1" +libraryDependencies += "dev.zio" %% "zio-json" % "0.6.2" ``` ## Example From 9b25d80f45fabedfe3c441b686c98a29d587b44e Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:01:20 +0100 Subject: [PATCH 003/311] Update magnolia to 1.1.8 (#1057) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index c01b8261a..323b0263c 100644 --- a/build.sbt +++ b/build.sbt @@ -123,7 +123,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) case _ => Vector( "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, - "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.3", + "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.8", "io.circe" %%% "circe-generic-extras" % circeVersion % "test", "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.23.3" % "test", "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.23.3" % "test" From 9dee4998e6c05d0e9639607cb5beacca6a19ab9a Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:08:01 +0100 Subject: [PATCH 004/311] Update nscplugin, sbt-scala-native, ... to 0.4.17 (#1063) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a12b7bd90..163cdea86 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.7") From 299766e940b1f7ff2636441e5ec2c1ba6e345095 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:40:42 +0100 Subject: [PATCH 005/311] Update sbt to 1.9.9 (#1073) --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index 304098715..04267b14a 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.4 +sbt.version=1.9.9 From 8f0ddd3d0d27961edc1e0fc1a561fa63217f15d8 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:42:18 +0100 Subject: [PATCH 006/311] Update silencer-plugin to 1.7.16 (#1070) --- project/BuildHelper.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 566e0df30..ee6bdf1a6 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -22,7 +22,7 @@ object BuildHelper { val Scala213: String = versions("2.13") val ScalaDotty: String = "3.3.0" - val SilencerVersion = "1.7.13" + val SilencerVersion = "1.7.16" private val stdOptions = Seq( "-deprecation", From 2fbf863b9d90baca98ec0460b88705d25c678c87 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:44:56 +0100 Subject: [PATCH 007/311] Update sbt-scalajs, scalajs-compiler, ... to 1.15.0 (#1060) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 163cdea86..7cdd2828a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,7 @@ addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.1") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From ede8742a66c4e8b7fa12a164bc9dc5fddbe7af8c Mon Sep 17 00:00:00 2001 From: Alex Demidov Date: Mon, 18 Mar 2024 23:45:42 +0700 Subject: [PATCH 008/311] Fixes jsonExclude annotation for Scala 3 (#1054) --- zio-json/shared/src/main/scala-3/zio/json/macros.scala | 3 +-- zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 31012a6ba..134711172 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -521,8 +521,7 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => val len = params.length val names = - IArray.genericWrapArray(ctx - .params + IArray.genericWrapArray(params .map { p => p.annotations.collectFirst { case jsonField(name) => name diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index 92c8fab59..77bf941ab 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -315,7 +315,7 @@ object EncoderSpec extends ZIOSpecDefault { }, test("exclude fields") { import exampleexcludefield._ - assert(Person("Peter", 20).toJson)(equalTo("""{"name":"Peter"}""")) + assert(Person(7, "Peter", 20).toJson)(equalTo("""{"name":"Peter"}""")) }, test("aliases") { import exampleproducts._ @@ -465,7 +465,7 @@ object EncoderSpec extends ZIOSpecDefault { object exampleexcludefield { - case class Person(name: String, @jsonExclude age: Int) + case class Person(@jsonExclude id: Long, name: String, @jsonExclude age: Int) object Person { implicit val encoder: JsonEncoder[Person] = DeriveJsonEncoder.gen[Person] From 93189987bc3b74fb0fc808192352e6b830276a0f Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:46:12 +0100 Subject: [PATCH 009/311] Update http4s-dsl to 0.23.26 (#1079) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 323b0263c..1a6f2e64d 100644 --- a/build.sbt +++ b/build.sbt @@ -319,7 +319,7 @@ lazy val zioJsonInteropHttp4s = project .settings( crossScalaVersions -= ScalaDotty, libraryDependencies ++= Seq( - "org.http4s" %% "http4s-dsl" % "0.23.20", + "org.http4s" %% "http4s-dsl" % "0.23.26", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.4.9", "dev.zio" %% "zio-interop-cats" % "23.0.03" % "test", From 0a2e119d009ec1db12e8c88d9d32db01d558f810 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:47:52 +0100 Subject: [PATCH 010/311] Update sbt-scoverage to 2.0.11 (#1075) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 7cdd2828a..a674e3a78 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -9,7 +9,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.7") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.3.10") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.7" From 1b4f797152f1957240b0ebc2c617708770c893d0 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:47:59 +0100 Subject: [PATCH 011/311] Update scala3-library, ... to 3.3.3 (#1078) --- .github/workflows/ci.yml | 2 +- project/BuildHelper.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5b0c4b06..05404d3eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: fail-fast: false matrix: java: ['8', '11', '17'] - scala: ['2.12.18', '2.13.11', '3.3.0'] + scala: ['2.12.18', '2.13.11', '3.3.3'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index ee6bdf1a6..1a68c2b8e 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -20,7 +20,7 @@ object BuildHelper { } val Scala212: String = versions("2.12") val Scala213: String = versions("2.13") - val ScalaDotty: String = "3.3.0" + val ScalaDotty: String = "3.3.3" val SilencerVersion = "1.7.16" From 9e995ef040856bff781955593fb8b00fd02397b7 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 18 Mar 2024 17:48:48 +0100 Subject: [PATCH 012/311] Update silencer-lib, silencer-lib_2.13.11 to 1.7.16 (#1072) From 78e375a2a655cb9cccded2b66ba0e04d62152f02 Mon Sep 17 00:00:00 2001 From: Ferdinand Svehla Date: Tue, 19 Mar 2024 09:39:18 +0100 Subject: [PATCH 013/311] Version updates, GH action updates, fix CI (#1080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use non-deprecated actions/checkout action * Update Scala 2.13 version to 2.13.13 * update actions/setup-java * update kind-projector, semanticdb * update play-json-extensions to fix JMH * fix fatal warnings * format sbt build file * CI: don’t compile benchmarks under JDK 8 * -Wconf boolean literals * Update ZIO --- .github/workflows/ci.yml | 28 +++++++++---------- build.sbt | 23 +++++++++++---- project/BuildHelper.scala | 7 +++-- .../scala/zio/json/yaml/YamlEncoderSpec.scala | 2 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05404d3eb..2bfc67eff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,11 @@ jobs: timeout-minutes: 30 steps: - name: Checkout current branch - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.1.2 with: fetch-depth: 0 - name: Setup Java - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.2.1 with: distribution: temurin java-version: 11 @@ -37,15 +37,15 @@ jobs: strategy: fail-fast: false matrix: - java: ['8', '11', '17'] - scala: ['2.13.11'] + java: ['11', '17'] + scala: ['2.13.13'] steps: - name: Checkout current branch - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.1.2 with: fetch-depth: 0 - name: Setup Java - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.2.1 with: distribution: temurin java-version: ${{ matrix.java }} @@ -60,9 +60,9 @@ jobs: timeout-minutes: 60 steps: - name: Checkout current branch - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.1.2 - name: Setup Java - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.2.1 with: distribution: temurin java-version: 8 @@ -78,16 +78,16 @@ jobs: strategy: fail-fast: false matrix: - java: ['8', '11', '17'] - scala: ['2.12.18', '2.13.11', '3.3.3'] + java: ['11', '17'] + scala: ['2.12.19', '2.13.13', '3.3.3'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.1.2 with: fetch-depth: 0 - name: Setup Java - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.2.1 with: distribution: temurin java-version: ${{ matrix.java }} @@ -117,11 +117,11 @@ jobs: if: github.event_name != 'pull_request' steps: - name: Checkout current branch - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.1.2 with: fetch-depth: 0 - name: Setup Java - uses: actions/setup-java@v3.12.0 + uses: actions/setup-java@v4.2.1 with: distribution: temurin java-version: 8 diff --git a/build.sbt b/build.sbt index 1a6f2e64d..ec2eff059 100644 --- a/build.sbt +++ b/build.sbt @@ -55,7 +55,7 @@ addCommandAlias( "zioJsonNative/test; zioJsonInteropScalaz7xNative/test" ) -val zioVersion = "2.0.16" +val zioVersion = "2.0.21" lazy val zioJsonRoot = project .in(file(".")) @@ -230,12 +230,23 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.typelevel" %% "jawn-ast" % "1.5.1" % "test" ) + case Some((2, n)) => + if (n >= 13) { + Seq( + "com.particeep" %% "play-json-extensions" % "0.43.1" % "test", + "com.typesafe.play" %%% "play-json" % "2.9.4" % "test", + "org.typelevel" %% "jawn-ast" % "1.5.1" % "test" + ) + } else { + Seq( + "ai.x" %% "play-json-extensions" % "0.42.0" % "test", + "com.typesafe.play" %%% "play-json" % "2.9.4" % "test", + "org.typelevel" %% "jawn-ast" % "1.5.1" % "test" + ) + } + case _ => - Seq( - "ai.x" %% "play-json-extensions" % "0.42.0" % "test", - "com.typesafe.play" %%% "play-json" % "2.9.4" % "test", - "org.typelevel" %% "jawn-ast" % "1.5.1" % "test" - ) + Seq.empty } } ) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 1a68c2b8e..9fb861450 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -146,7 +146,8 @@ object BuildHelper { ) case Some((2, 13)) => Seq( - "-Ywarn-unused:params,-implicits" + "-Ywarn-unused:params,-implicits", + "-Wconf:msg=Boolean literals should be passed:s" ) ++ std2xOptions ++ optimizerOptions(optimize) case Some((2, 12)) => Seq( @@ -223,12 +224,12 @@ object BuildHelper { Seq( "com.github.ghik" % "silencer-lib" % SilencerVersion % Provided cross CrossVersion.full, compilerPlugin("com.github.ghik" % "silencer-plugin" % SilencerVersion cross CrossVersion.full), - compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full) + compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.3" cross CrossVersion.full) ) }, semanticdbEnabled := scalaVersion.value != ScalaDotty, // enable SemanticDB semanticdbOptions += "-P:semanticdb:synthetics:on", - semanticdbVersion := "4.8.7", + semanticdbVersion := "4.9.2", Test / parallelExecution := true, incOptions ~= (_.withLogRecompileOnMacro(false)), autoAPIMappings := true, diff --git a/zio-json-yaml/src/test/scala/zio/json/yaml/YamlEncoderSpec.scala b/zio-json-yaml/src/test/scala/zio/json/yaml/YamlEncoderSpec.scala index 8a053aee1..494a17584 100644 --- a/zio-json-yaml/src/test/scala/zio/json/yaml/YamlEncoderSpec.scala +++ b/zio-json-yaml/src/test/scala/zio/json/yaml/YamlEncoderSpec.scala @@ -70,7 +70,7 @@ object YamlEncoderSpec extends ZIOSpecDefault { test("sequence root") { assert( Json - .Arr(Json.Bool(true), Json.Bool(false), Json.Bool(true)) + .Arr(Json.Bool.True, Json.Bool.False, Json.Bool.True) .toYaml(YamlOptions.default.copy(lineBreak = LineBreak.UNIX)) )( isRight(equalTo(""" - true From 121fe3a5464271f890fd616ef184f53cd3ad8d37 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 21 Mar 2024 06:51:42 +0100 Subject: [PATCH 014/311] Update sbt-scalajs, scalajs-compiler, ... to 1.16.0 (#1082) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a674e3a78..7145e9c95 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,7 @@ addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.1") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From b9cab26df29222ea00a98737810ba7c854310381 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 21 Mar 2024 06:54:22 +0100 Subject: [PATCH 015/311] Update sbt-buildinfo to 0.12.0 (#1081) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 7145e9c95..afe456fcf 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.11") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.1") From e1f34bda8180539125db9f47ea44c7615aa216f6 Mon Sep 17 00:00:00 2001 From: Zoran Jeremic Date: Mon, 15 Apr 2024 10:32:51 -0700 Subject: [PATCH 016/311] Updating Decoding doc with example of writing custom decoder (#1089) --- docs/decoding.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/decoding.md b/docs/decoding.md index e17cbfa42..e7a9ffe7c 100644 --- a/docs/decoding.md +++ b/docs/decoding.md @@ -174,3 +174,47 @@ implicit val decodeName: JsonDecoder[String Refined NonEmpty] = ``` Now the code compiles. + +### Writing a Custom Decoder +In some rare cases, you might encounter situations where the data format deviates from the expected structure. + +#### Problem +Let's consider an Animal case class with a categories field that should be a list of strings. However, some JSON data might represent the categories as a comma-separated string instead of a proper list. + +```scala mdoc +import zio.json.ast.Json +case class Animal(name: String, categories: List[String]) +``` + + +#### The Solution: Custom Decoder + +We can create custom decoders to handle specific data formats. Here's an implementation for our Animal case class: +```scala mdoc +object Animal { + implicit val decoder: JsonDecoder[Animal] = JsonDecoder[Json].mapOrFail { + case Json.Obj(fields) => + (for { + name <- fields.find(_._1 == "name").map(_._2.toString()) + categories <- fields + .find(_._1 == "categories").map(_._2.toString()) + } yield Right(Animal(name, handleCategories(categories)))) + .getOrElse(Left("DecodingError")) + case _ => Left("Error") + } + + private def handleCategories(categories: String): List[String] = { + val decodedList = JsonDecoder[List[String]].decodeJson(categories) + decodedList match { + case Right(list) => list + case Left(_) => + categories.replaceAll("\"", "").split(",").toList + } + } +} +``` +And now, JsonDecoder for Animal can handle both formats: +``` scala mdoc +"""{"name": "Dog", "categories": "Warm-blooded, Mammal"}""".fromJson[Animal] +"""{"name": "Snake", "categories": [ "Cold-blooded", "Reptile"]}""".fromJson[Animal] +``` From 0cd8bb874de76be5e764884a3ef79f300d10d82b Mon Sep 17 00:00:00 2001 From: counter2015 Date: Wed, 24 Apr 2024 17:55:58 +0800 Subject: [PATCH 017/311] fix some warnings. (#1095) --- project/NeoJmhPlugin.scala | 30 +++++++++---------- .../jmh/scala/zio/json/UUIDBenchmarks.scala | 2 +- .../json/JsonDecoderPlatformSpecific.scala | 4 +-- .../json/JsonEncoderPlatformSpecific.scala | 2 +- .../src/main/scala-2.x/zio/json/macros.scala | 7 ++--- .../src/main/scala/zio/json/JsonEncoder.scala | 1 - .../src/main/scala/zio/json/ast/ast.scala | 2 +- .../src/test/scala/zio/json/CodecSpec.scala | 1 - .../src/test/scala/zio/json/DecoderSpec.scala | 2 +- 9 files changed, 24 insertions(+), 27 deletions(-) diff --git a/project/NeoJmhPlugin.scala b/project/NeoJmhPlugin.scala index 05a51e66e..a831c348b 100644 --- a/project/NeoJmhPlugin.scala +++ b/project/NeoJmhPlugin.scala @@ -52,7 +52,7 @@ object NeoJmhPlugin extends AutoPlugin { override def projectSettings = inConfig(Jmh)( Defaults.testSettings ++ Seq( - run := (run in JmhInternal).evaluated, + run := (JmhInternal / run).evaluated, neoJmhGenerator := "reflection", neoJmhYourkit := Nil, javaOptions ++= Seq( @@ -71,11 +71,11 @@ object NeoJmhPlugin extends AutoPlugin { ) ) ++ inConfig(JmhInternal)( Defaults.testSettings ++ Seq( - javaOptions := (javaOptions in Jmh).value, - envVars := (envVars in Jmh).value, - mainClass in run := Some("org.openjdk.jmh.Main"), - fork in run := true, - dependencyClasspath ++= (fullClasspath in Jmh).value, + javaOptions := (Jmh / javaOptions).value, + envVars := (Jmh / envVars).value, + run / mainClass := Some("org.openjdk.jmh.Main"), + run / fork := true, + dependencyClasspath ++= (Jmh / fullClasspath).value, sourceGenerators += generateJmhSourcesAndResources.map { case (sources, _) => sources }, @@ -106,23 +106,23 @@ object NeoJmhPlugin extends AutoPlugin { def backCompatProjectSettings: Seq[Setting[_]] = Seq( // WORKAROUND https://github.com/sbt/sbt/issues/3935 - dependencyClasspathAsJars in NeoJmhPlugin.JmhInternal ++= (fullClasspathAsJars in NeoJmhKeys.Jmh).value + NeoJmhPlugin.JmhInternal / dependencyClasspathAsJars ++= (NeoJmhKeys.Jmh / fullClasspathAsJars).value ) def generateBenchmarkSourcesAndResources: Def.Initialize[Task[(Seq[File], Seq[File])]] = Def.task { val s = streams.value val cacheDir = crossTarget.value / "jmh-cache" - val bytecodeDir = (classDirectory in Jmh).value + val bytecodeDir = (Jmh / classDirectory).value val sourceDir = sourceManaged.value val resourceDir = resourceManaged.value - val generator = (neoJmhGenerator in Jmh).value + val generator = (Jmh / neoJmhGenerator).value val classpath = dependencyClasspath.value - val javaHomeV = (javaHome in Jmh).value - val outputStrategyV = (outputStrategy in Jmh).value - val workingDirectory = Option((baseDirectory in Jmh).value) - val connectInputV = (connectInput in Jmh).value - val envVarsV = (envVars in Jmh).value - val javaFlags = (javaOptions in Jmh).value.toVector + val javaHomeV = (Jmh / javaHome).value + val outputStrategyV = (Jmh / outputStrategy).value + val workingDirectory = Option((Jmh / baseDirectory).value) + val connectInputV = (Jmh / connectInput).value + val envVarsV = (Jmh / envVars).value + val javaFlags = (Jmh / javaOptions).value.toVector val inputs: Set[File] = (bytecodeDir ** "*").filter(_.isFile).get.toSet val cachedGeneration = FileFunction.cached(cacheDir, FilesInfo.hash) { _ => diff --git a/zio-json/jvm/src/jmh/scala/zio/json/UUIDBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/UUIDBenchmarks.scala index 6b987e219..3912c108a 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/UUIDBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/UUIDBenchmarks.scala @@ -30,7 +30,7 @@ class UUIDBenchmarks { } yield s"$s1-$s2-$s3-$s4-$s5" unparsedUUIDChunk = { - Unsafe.unsafeCompat { implicit u => + Unsafe.unsafe { implicit u => zio.Runtime.default.unsafe.run(gen.runCollectN(10000).map(Chunk.fromIterable)).getOrThrow() } } diff --git a/zio-json/jvm/src/main/scala/zio/json/JsonDecoderPlatformSpecific.scala b/zio-json/jvm/src/main/scala/zio/json/JsonDecoderPlatformSpecific.scala index 3394cf0d5..11da0dbfd 100644 --- a/zio-json/jvm/src/main/scala/zio/json/JsonDecoderPlatformSpecific.scala +++ b/zio-json/jvm/src/main/scala/zio/json/JsonDecoderPlatformSpecific.scala @@ -54,7 +54,7 @@ trait JsonDecoderPlatformSpecific[A] { self: JsonDecoder[A] => final def decodeJsonPipeline( delimiter: JsonStreamDelimiter = JsonStreamDelimiter.Array ): ZPipeline[Any, Throwable, Char, A] = { - Unsafe.unsafeCompat { (u: Unsafe) => + Unsafe.unsafe { (u: Unsafe) => implicit val unsafe: Unsafe = u ZPipeline.fromPush { @@ -121,7 +121,7 @@ trait JsonDecoderPlatformSpecific[A] { self: JsonDecoder[A] => throw new Exception(JsonError.render(trace)) } - Unsafe.unsafeCompat { (u: Unsafe) => + Unsafe.unsafe { (u: Unsafe) => implicit val unsafe: Unsafe = u runtime.unsafe.run(outQueue.offer(Take.single(nextElem))).getOrThrow() diff --git a/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala b/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala index bd4b995f6..516fc41fd 100644 --- a/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala +++ b/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala @@ -17,7 +17,7 @@ trait JsonEncoderPlatformSpecific[A] { self: JsonEncoder[A] => delimiter: Option[Char], endWith: Option[Char] ): ZPipeline[Any, Throwable, A, Char] = - Unsafe.unsafeCompat { (u: Unsafe) => + Unsafe.unsafe { (u: Unsafe) => implicit val unsafe: Unsafe = u ZPipeline.fromPush { diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 3810b8c58..6be618f0e 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -9,7 +9,6 @@ import zio.json.ast.Json import zio.json.internal.{ Lexer, RetractReader, StringMatrix, Write } import scala.annotation._ -import scala.collection.mutable import scala.language.experimental.macros /** @@ -309,7 +308,7 @@ object DeriveJsonDecoder { val len: Int = names.length val matrix: StringMatrix = new StringMatrix(names, aliases) - val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_)) + val spans: Array[JsonError] = names.map(JsonError.ObjectAccess) lazy val tcs: Array[JsonDecoder[Any]] = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val defaults: Array[Option[Any]] = @@ -431,7 +430,7 @@ object DeriveJsonDecoder { ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) if (discrim.isEmpty) new JsonDecoder[A] { - val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_)) + val spans: Array[JsonError] = names.map(JsonError.ObjectAccess) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') // we're not allowing extra fields in this encoding @@ -469,7 +468,7 @@ object DeriveJsonDecoder { new JsonDecoder[A] { val hintfield = discrim.get val hintmatrix = new StringMatrix(Array(hintfield)) - val spans: Array[JsonError] = names.map(JsonError.Message(_)) + val spans: Array[JsonError] = names.map(JsonError.Message) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val in_ = internal.RecordingReader(in) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index d118674de..cb2882b56 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -80,7 +80,6 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { * This default may be overridden when this value may be missing within a JSON object and still * be encoded. */ - @nowarn("msg=is never used") def isNothing(a: A): Boolean = false /** diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index d251165f6..5d2f1f900 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -493,7 +493,7 @@ object Json { def apply(value: Int): Num = Num(BigDecimal(value).bigDecimal) def apply(value: Long): Num = Num(BigDecimal(value).bigDecimal) def apply(value: BigDecimal): Num = Num(value.bigDecimal) - def apply(value: Float): Num = Num(BigDecimal(value).bigDecimal) + def apply(value: Float): Num = Num(BigDecimal.decimal(value).bigDecimal) def apply(value: Double): Num = Num(BigDecimal(value).bigDecimal) implicit val decoder: JsonDecoder[Num] = new JsonDecoder[Num] { diff --git a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala index c05d08986..2e2c5a12f 100644 --- a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala @@ -92,7 +92,6 @@ object CodecSpec extends ZIOSpecDefault { val snaked = """{"indiana123_jones":""}""" val pascaled = """{"Anders123Hejlsberg":""}""" val cameled = """{"small123Talk":""}""" - val indianaJones = """{"wHATcASEiStHIS":""}""" val overrides = """{"not_modified":"","but-this-should-be":0}""" val kebabedLegacy = """{"shish-123-kebab":""}""" val snakedLegacy = """{"indiana_123_jones":""}""" diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 358352882..e2fd0b796 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -5,7 +5,7 @@ import zio.json._ import zio.json.ast.Json import zio.test.Assertion._ import zio.test.TestAspect.jvmOnly -import zio.test.{ TestAspect, _ } +import zio.test._ import java.time.{ Duration, OffsetDateTime, ZonedDateTime } import java.util.UUID From 3e155fa1409405139ea457a40a069636acd0ee23 Mon Sep 17 00:00:00 2001 From: Erik van Oosten Date: Wed, 24 Apr 2024 11:57:37 +0200 Subject: [PATCH 018/311] More custom decoding documentation (#1094) * Lots of options for custom decoding Based on Discord discussion https://discord.com/channels/629491597070827530/733728086637412422/1231658233052008448 * Fix mdoc * Fix language --- docs/decoding.md | 196 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 169 insertions(+), 27 deletions(-) diff --git a/docs/decoding.md b/docs/decoding.md index e7a9ffe7c..5a72f12c5 100644 --- a/docs/decoding.md +++ b/docs/decoding.md @@ -175,46 +175,188 @@ implicit val decodeName: JsonDecoder[String Refined NonEmpty] = Now the code compiles. -### Writing a Custom Decoder -In some rare cases, you might encounter situations where the data format deviates from the expected structure. +# Parsing custom JSON -#### Problem -Let's consider an Animal case class with a categories field that should be a list of strings. However, some JSON data might represent the categories as a comma-separated string instead of a proper list. +In this section we show several approaches for decoding JSON that looks like: + +```json +{ + "01. symbol": "IBM", + "02. open": "182.4300", + "03. high": "182.8000" +} +``` + +Which we want to decode into the following case class: ```scala mdoc +final case class Quote( + symbol: String, + open: String, + high: String +) +``` + +All approaches have the same result: + +```scala mdoc:fail +"""{"01. symbol":"IBM","02. open": "182.4300","03. high": "182.8000"}""".fromJson[Quote] +// >> Right(Quote(IBM,182.4300,182.8000)) +``` + +## Approach 1: use annotation hints + +In this approach we enrich the case class with annotations to tell the derived decoder which field names to use. +Obviously, this approach only works if we can/want to change the case class. + +```scala mdoc:reset +import zio.json._ + +final case class Quote( + @jsonField("01. symbol") symbol: String, + @jsonField("02. open") open: String, + @jsonField("03. high") high: String +) + +object Quote { + implicit val decoder: JsonDecoder[Quote] = DeriveJsonDecoder.gen[Quote] +} +``` + +## Approach 2: use an intermediate case class + +Instead of hints, we can also put the actual field names in an intermediate case class. In our example the field names +are not valid scala identifiers. We fix this by putting the names in backticks: + +```scala mdoc:reset +import zio.json._ + +final case class Quote(symbol: String, open: String, high: String) + +object Quote { + private final case class JsonQuote( + `01. symbol`: String, + `02. open`: String, + `03. high`: String + ) + + implicit val decoder: JsonDecoder[Quote] = + DeriveJsonDecoder + .gen[JsonQuote] + .map { case JsonQuote(s, o, h) => Quote(s, o, h) } +} +``` + +## Approach 3: decode to JSON + +In this approach we first decode to the generic `Json` data structure. This approach is very flexible because it can +extract data from any valid JSON. + +Note that this implementation is a bit sloppy. It uses `toString` on a JSON node. The node is not necessarily a +String, it can be of any JSON type! So this might happily process JSON that doesn't match your expectations. + +```scala mdoc:reset +import zio.json._ import zio.json.ast.Json -case class Animal(name: String, categories: List[String]) + +final case class Quote(symbol: String, open: String, high: String) + +object Quote { + implicit val decoder: JsonDecoder[Quote] = JsonDecoder[Json] + .mapOrFail { + case Json.Obj(fields) => + def findField(name: String): Either[String, String] = + fields + .find(_._1 == name) + .map(_._2.toString()) // ⚠️ .toString on any JSON type + .toRight(left = s"Field '$name' is missing") + + for { + symbol <- findField("01. symbol") + open <- findField("02. open") + high <- findField("03. high") + } yield Quote(symbol, open, high) + case _ => + Left("Not a JSON record") + } +} ``` +## Approach 4: decode to JSON, use cursors -#### The Solution: Custom Decoder +Here we also first decode to `Json`, but now we use cursors to find the data we need. Here we do check that the fields +are actually strings. -We can create custom decoders to handle specific data formats. Here's an implementation for our Animal case class: -```scala mdoc -object Animal { - implicit val decoder: JsonDecoder[Animal] = JsonDecoder[Json].mapOrFail { - case Json.Obj(fields) => - (for { - name <- fields.find(_._1 == "name").map(_._2.toString()) - categories <- fields - .find(_._1 == "categories").map(_._2.toString()) - } yield Right(Animal(name, handleCategories(categories)))) - .getOrElse(Left("DecodingError")) - case _ => Left("Error") +```scala mdoc:reset +import zio.json._ +import zio.json.ast.{Json, JsonCursor} + +final case class Quote(symbol: String, open: String, high: String) + +object Quote { + private val symbolC = JsonCursor.field("01. symbol") >>> JsonCursor.isString + private val openC = JsonCursor.field("02. open") >>> JsonCursor.isString + private val highC = JsonCursor.field("03. high") >>> JsonCursor.isString + + implicit val decoder: JsonDecoder[Quote] = JsonDecoder[Json] + .mapOrFail { c => + for { + symbol <- c.get(symbolC) + open <- c.get(openC) + high <- c.get(highC) + } yield Quote(symbol.value, open.value, high.value) } +} +``` - private def handleCategories(categories: String): List[String] = { - val decodedList = JsonDecoder[List[String]].decodeJson(categories) - decodedList match { - case Right(list) => list - case Left(_) => - categories.replaceAll("\"", "").split(",").toList +# More custom decoder examples + +Let's consider an `Animal` case class with a `categories` field that should be a list of strings. However, some +producers accidentally represent the categories as a comma-separated string instead of a proper list. We want to parse +both cases. + +Here's a custom decode for our Animal case class: + +```scala mdoc:reset +import zio.Chunk +import zio.json._ +import zio.json.ast._ + +case class Animal(name: String, categories: List[String]) + +object Animal { + private val nameC = JsonCursor.field("name") >>> JsonCursor.isString + private val categoryArrayC = JsonCursor.field("categories") >>> JsonCursor.isArray + private val categoryStringC = JsonCursor.field("categories") >>> JsonCursor.isString + + implicit val decoder: JsonDecoder[Animal] = JsonDecoder[Json] + .mapOrFail { c => + for { + name <- c.get(nameC).map(_.value) + categories <- arrayCategory(c).map(_.toList) + .orElse(c.get(categoryStringC).map(_.value.split(',').map(_.trim).toList)) + } yield Animal(name, categories) + } + + private def arrayCategory(c: Json): Either[String, Chunk[String]] = + c.get(categoryArrayC) + .flatMap { arr => + // Get the string elements, and sequence the obtained eithers to a single either + sequence(arr.elements.map(_.get(JsonCursor.isString).map(_.value))) + } + + private def sequence[A, B](chunk: Chunk[Either[A, B]]): Either[A, Chunk[B]] = + chunk.partition(_.isLeft) match { + case (Nil, rights) => Right(rights.collect { case Right(r) => r }) + case (lefts, _) => Left(lefts.collect { case Left(l) => l }.head) } - } } ``` -And now, JsonDecoder for Animal can handle both formats: -``` scala mdoc + +And now, the Json decoder for Animal can handle both formats: +```scala mdoc """{"name": "Dog", "categories": "Warm-blooded, Mammal"}""".fromJson[Animal] +// >> Right(Animal(Dog,List(Warm-blooded, Mammal))) """{"name": "Snake", "categories": [ "Cold-blooded", "Reptile"]}""".fromJson[Animal] +// >> Right(Animal(Snake,List(Cold-blooded, Reptile))) ``` From bc86948d5a5e30a435c632803d59b5adf731c4bf Mon Sep 17 00:00:00 2001 From: Vladimir Klyushnikov <72238+vladimirkl@users.noreply.github.com> Date: Thu, 25 Apr 2024 19:34:25 +0300 Subject: [PATCH 019/311] Type hint transformation for sealed hierarchies (#1093) --- docs/configuration.md | 26 ++++++++++ .../src/test/scala/zio/json/DeriveSpec.scala | 37 ++++++++++++++ .../src/main/scala-2.x/zio/json/macros.scala | 18 +++++-- .../src/main/scala-3/zio/json/macros.scala | 21 ++++++-- .../json/ConfigurableDeriveCodecSpec.scala | 13 +++++ .../src/test/scala/zio/json/CodecSpec.scala | 43 +++++++++++++++++ .../src/test/scala/zio/json/DecoderSpec.scala | 48 +++++++++++++++++++ .../src/test/scala/zio/json/EncoderSpec.scala | 48 +++++++++++++++++++ 8 files changed, 247 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 3c7da6f6f..87631ca8b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -80,6 +80,32 @@ banana.toJson apple.toJson ``` +Another way of changing type hint is using `@jsonHintNames` annotation on sealed class. It allows to apply transformation +to all type hint values in hierarchy. Same transformations are provided as for `@jsonMemberNames` annotation. + +Here's an example: + +```scala mdoc +import zio.json._ + +@jsonHintNames(SnakeCase) +sealed trait FruitKind + +case class GoodFruit(good: Boolean) extends FruitKind + +case class BadFruit(bad: Boolean) extends FruitKind + +object FruitKind { + implicit val encoder: JsonEncoder[FruitKind] = + DeriveJsonEncoder.gen[FruitKind] +} + +val goodFruit: FruitKind = GoodFruit(true) +val badFruit: FruitKind = BadFruit(true) + +goodFruit.toJson +badFruit.toJson +``` ## jsonDiscriminator diff --git a/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala b/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala index d62fd4d60..c88f87a12 100644 --- a/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala +++ b/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala @@ -31,6 +31,13 @@ object DeriveSpec extends ZIOSpecDefault { assert("""{"Child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2()))) && assert("""{"type":"Child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) }, + test("sum encoding with hint names") { + import examplesumhintnames._ + + assert("""{"child1":{}}""".fromJson[Parent])(isRight(equalTo(Child1()))) && + assert("""{"child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2()))) && + assert("""{"type":"child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) + }, test("sum alternative encoding") { import examplealtsum._ @@ -38,6 +45,14 @@ object DeriveSpec extends ZIOSpecDefault { assert("""{"hint":"Abel"}""".fromJson[Parent])(isRight(equalTo(Child2()))) && assert("""{"hint":"Samson"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && assert("""{"Cain":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) + }, + test("sum alternative encoding with hint names") { + import examplealtsumhintnames._ + + assert("""{"hint":"child1"}""".fromJson[Parent])(isRight(equalTo(Child1()))) && + assert("""{"hint":"Abel"}""".fromJson[Parent])(isRight(equalTo(Child2()))) && + assert("""{"hint":"Child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && + assert("""{"child1":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) } ) ) @@ -59,6 +74,15 @@ object DeriveSpec extends ZIOSpecDefault { case class Child2() extends Parent } + object examplesumhintnames { + @jsonDerive + @jsonHintNames(SnakeCase) + sealed abstract class Parent + + case class Child1() extends Parent + case class Child2() extends Parent + } + object exampleempty { @jsonDerive case class Empty(a: Option[String]) @@ -78,6 +102,19 @@ object DeriveSpec extends ZIOSpecDefault { case class Child2() extends Parent } + object examplealtsumhintnames { + + @jsonDerive + @jsonDiscriminator("hint") + @jsonHintNames(SnakeCase) + sealed abstract class Parent + + case class Child1() extends Parent + + @jsonHint("Abel") + case class Child2() extends Parent + } + object logEvent { @jsonDerive(JsonDeriveConfig.Decoder) case class Event(at: Long, message: String, a: Seq[String] = Nil) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 6be618f0e..823687d29 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -175,6 +175,12 @@ private[json] object jsonMemberNames { */ final case class jsonHint(name: String) extends Annotation +/** + * If used on a sealed class will determine the strategy of type hint value transformation for disambiguating + * classes during serialization and deserialization. Same strategies are provided as for [[jsonMemberNames]]. + */ +final case class jsonHintNames(format: JsonMemberFormat) extends Annotation + /** * If used on a case class, will exit early if any fields are in the JSON that * do not correspond to field names in the case class. @@ -200,11 +206,13 @@ final class jsonExclude extends Annotation * @param sumTypeHandling see [[jsonDiscriminator]] * @param fieldNameMapping see [[jsonMemberNames]] * @param allowExtraFields see [[jsonNoExtraFields]] + * @param sumTypeMapping see [[jsonHintNames]] */ final case class JsonCodecConfiguration( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, fieldNameMapping: JsonMemberFormat = IdentityFormat, - allowExtraFields: Boolean = true + allowExtraFields: Boolean = true, + sumTypeMapping: JsonMemberFormat = IdentityFormat ) object JsonCodecConfiguration { @@ -416,10 +424,12 @@ object DeriveJsonDecoder { } def split[A](ctx: SealedTrait[JsonDecoder, A])(implicit config: JsonCodecConfiguration): JsonDecoder[A] = { + val jsonHintFormat: JsonMemberFormat = + ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val names: Array[String] = ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name - }.getOrElse(p.typeName.short) + }.getOrElse(jsonHintFormat(p.typeName.short)) }.toArray val matrix: StringMatrix = new StringMatrix(names) lazy val tcs: Array[JsonDecoder[Any]] = @@ -594,10 +604,12 @@ object DeriveJsonEncoder { } def split[A](ctx: SealedTrait[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = { + val jsonHintFormat: JsonMemberFormat = + ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val names: Array[String] = ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name - }.getOrElse(p.typeName.short) + }.getOrElse(jsonHintFormat(p.typeName.short)) }.toArray def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 134711172..d61500909 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -64,6 +64,9 @@ case object PascalCase extends JsonMemberFormat { case object KebabCase extends JsonMemberFormat { override def apply(memberName: String): String = jsonMemberNames.enforceSnakeOrKebabCase(memberName, '-') } +case object IdentityFormat extends JsonMemberFormat { + override def apply(memberName: String): String = memberName +} /** zio-json version 0.3.0 formats. abc123Def -> abc_123_def */ object ziojson_03 { @@ -175,6 +178,12 @@ private[json] object jsonMemberNames { */ final case class jsonHint(name: String) extends Annotation +/** + * If used on a sealed class will determine the strategy of type hint value transformation for disambiguating + * classes during serialization and deserialization. Same strategies are provided as for [[jsonMemberNames]]. + */ +final case class jsonHintNames(format: JsonMemberFormat) extends Annotation + /** * If used on a case class, will exit early if any fields are in the JSON that * do not correspond to field names in the case class. @@ -370,10 +379,12 @@ object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => } def split[A](ctx: SealedTrait[JsonDecoder, A]): JsonDecoder[A] = { + val jsonHintFormat: JsonMemberFormat = + ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(IdentityFormat) val names: Array[String] = IArray.genericWrapArray(ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name - }.getOrElse(p.typeInfo.short) + }.getOrElse(jsonHintFormat(p.typeInfo.short)) }).toArray val matrix: StringMatrix = new StringMatrix(names) @@ -594,6 +605,8 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => } def split[A](ctx: SealedTrait[JsonEncoder, A]): JsonEncoder[A] = { + val jsonHintFormat: JsonMemberFormat = + ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(IdentityFormat) val discrim = ctx .annotations .collectFirst { @@ -608,7 +621,7 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => .annotations .collectFirst { case jsonHint(name) => name - }.getOrElse(sub.typeInfo.short) + }.getOrElse(jsonHintFormat(sub.typeInfo.short)) out.write("{") val indent_ = JsonEncoder.bump(indent) @@ -635,7 +648,7 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => .annotations .collectFirst { case jsonHint(name) => name - }.getOrElse(sub.typeInfo.short) + }.getOrElse(jsonHintFormat(sub.typeInfo.short)) Json.Obj( Chunk( @@ -652,7 +665,7 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => def getName(annotations: Iterable[_], default: => String): String = annotations .collectFirst { case jsonHint(name) => name } - .getOrElse(default) + .getOrElse(jsonHintFormat(default)) new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { diff --git a/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala index b61ff8485..4f2094c07 100644 --- a/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala +++ b/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala @@ -113,6 +113,19 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { expectedObj.toJson == expectedStr ) }, + test("should override sum type mapping") { + val expectedStr = """{"$type":"case_class","i":1}""" + val expectedObj: ST = ST.CaseClass(i = 1) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type"), sumTypeMapping = SnakeCase) + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, test("should prevent extra fields") { val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" diff --git a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala index 2e2c5a12f..57f70b132 100644 --- a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala @@ -78,6 +78,13 @@ object CodecSpec extends ZIOSpecDefault { assert("""{"Child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2()))) && assert("""{"type":"Child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) }, + test("sum encoding with hint names") { + import examplesumhintnames._ + + assert("""{"child1":{}}""".fromJson[Parent])(isRight(equalTo(Child1()))) && + assert("""{"child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2()))) && + assert("""{"type":"child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) + }, test("sum alternative encoding") { import examplealtsum._ @@ -86,6 +93,14 @@ object CodecSpec extends ZIOSpecDefault { assert("""{"hint":"Samson"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && assert("""{"Cain":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) }, + test("sum alternative encoding with hint names") { + import examplealtsumhintnames._ + + assert("""{"hint":"child1"}""".fromJson[Parent])(isRight(equalTo(Child1()))) && + assert("""{"hint":"Abel"}""".fromJson[Parent])(isRight(equalTo(Child2()))) && + assert("""{"hint":"Child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && + assert("""{"child1":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) + }, test("key transformation") { import exampletransformkeys._ val kebabed = """{"shish123-kebab":""}""" @@ -231,6 +246,17 @@ object CodecSpec extends ZIOSpecDefault { case class Child2() extends Parent } + object examplesumhintnames { + @jsonHintNames(SnakeCase) + sealed abstract class Parent + + object Parent { + implicit val codec: JsonCodec[Parent] = DeriveJsonCodec.gen[Parent] + } + case class Child1() extends Parent + case class Child2() extends Parent + } + object exampleempty { case class Empty(a: Option[String]) @@ -242,6 +268,7 @@ object CodecSpec extends ZIOSpecDefault { object examplealtsum { @jsonDiscriminator("hint") + @jsonHintNames(SnakeCase) sealed abstract class Parent object Parent { @@ -255,6 +282,22 @@ object CodecSpec extends ZIOSpecDefault { case class Child2() extends Parent } + object examplealtsumhintnames { + + @jsonDiscriminator("hint") + @jsonHintNames(SnakeCase) + sealed abstract class Parent + + object Parent { + implicit val codec: JsonCodec[Parent] = DeriveJsonCodec.gen[Parent] + } + + case class Child1() extends Parent + + @jsonHint("Abel") + case class Child2() extends Parent + } + object exampletransformkeys { @jsonMemberNames(KebabCase) case class Kebabed(shish123Kebab: String) diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index e2fd0b796..acb23e86f 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -149,6 +149,14 @@ object DecoderSpec extends ZIOSpecDefault { assert("""{"Child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2()))) && assert("""{"type":"Child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) }, + test("sum encoding with hint names") { + import examplesumhintnames._ + + assert("""{"child1":{}}""".fromJson[Parent])(isRight(equalTo(Child1()))) && + assert("""{"child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2()))) && + assert("""{"Child1":{}}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && + assert("""{"type":"child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) + }, test("sum alternative encoding") { import examplealtsum._ @@ -157,6 +165,14 @@ object DecoderSpec extends ZIOSpecDefault { assert("""{"hint":"Samson"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && assert("""{"Cain":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) }, + test("sum alternative encoding with hint names") { + import examplealtsumhintnames._ + + assert("""{"hint":"child1"}""".fromJson[Parent])(isRight(equalTo(Child1()))) && + assert("""{"hint":"Abel"}""".fromJson[Parent])(isRight(equalTo(Child2()))) && + assert("""{"hint":"Child2"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && + assert("""{"child1":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) + }, test("unicode") { assert(""""€🐵🥰"""".fromJson[String])(isRight(equalTo("€🐵🥰"))) }, @@ -544,6 +560,21 @@ object DecoderSpec extends ZIOSpecDefault { } + object examplesumhintnames { + + @jsonHintNames(CamelCase) + sealed abstract class Parent + + object Parent { + implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] + } + + case class Child1() extends Parent + + case class Child2() extends Parent + + } + object examplealtsum { @jsonDiscriminator("hint") @@ -561,6 +592,23 @@ object DecoderSpec extends ZIOSpecDefault { } + object examplealtsumhintnames { + + @jsonDiscriminator("hint") + @jsonHintNames(CamelCase) + sealed abstract class Parent + + object Parent { + implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] + } + + case class Child1() extends Parent + + @jsonHint("Abel") + case class Child2() extends Parent + + } + object logEvent { case class Event(at: Long, message: String) diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index 77bf941ab..c8cd85791 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -400,6 +400,12 @@ object EncoderSpec extends ZIOSpecDefault { assert((Child1(): Parent).toJsonAST)(isRight(equalTo(Json.Obj(Chunk("Child1" -> Json.Obj()))))) && assert((Child2(): Parent).toJsonAST)(isRight(equalTo(Json.Obj(Chunk("Cain" -> Json.Obj()))))) }, + test("sum encoding with hint names") { + import examplesumhintnames._ + + assert((Child1(): Parent).toJsonAST)(isRight(equalTo(Json.Obj(Chunk("child1" -> Json.Obj()))))) && + assert((Child2(): Parent).toJsonAST)(isRight(equalTo(Json.Obj(Chunk("Cain" -> Json.Obj()))))) + }, test("sum alternative encoding") { import examplealtsum._ @@ -409,6 +415,15 @@ object EncoderSpec extends ZIOSpecDefault { (isRight(equalTo(Json.Obj("s" -> Json.Str("hello"), "hint" -> Json.Str("Abel"))))) ) }, + test("sum alternative encoding with hint names") { + import examplealtsumhintnames._ + + assert((Child1(): Parent).toJsonAST)(isRight(equalTo(Json.Obj("hint" -> Json.Str("child1"))))) && + assert((Child2(None): Parent).toJsonAST)(isRight(equalTo(Json.Obj("hint" -> Json.Str("Abel"))))) && + assert((Child2(Some("hello")): Parent).toJsonAST)( + (isRight(equalTo(Json.Obj("s" -> Json.Str("hello"), "hint" -> Json.Str("Abel"))))) + ) + }, test("alias") { import exampleproducts._ @@ -488,6 +503,22 @@ object EncoderSpec extends ZIOSpecDefault { } + object examplesumhintnames { + + @jsonHintNames(CamelCase) + sealed abstract class Parent + + object Parent { + implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] + } + + case class Child1() extends Parent + + @jsonHint("Cain") + case class Child2() extends Parent + + } + object examplealtsum { @jsonDiscriminator("hint") @@ -504,4 +535,21 @@ object EncoderSpec extends ZIOSpecDefault { } + object examplealtsumhintnames { + + @jsonDiscriminator("hint") + @jsonHintNames(CamelCase) + sealed abstract class Parent + + object Parent { + implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] + } + + case class Child1() extends Parent + + @jsonHint("Abel") + case class Child2(s: Option[String]) extends Parent + + } + } From 61b7634b1ea3d7062298c9ab8a4c5f996800fef0 Mon Sep 17 00:00:00 2001 From: Ezhil Shanmugham Date: Wed, 1 May 2024 22:23:18 +0530 Subject: [PATCH 020/311] fix: broken link (#1097) * fix: broken link --- .github/workflows/site.yml | 2 -- README.md | 10 +++++----- build.sbt | 1 - project/plugins.sbt | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 7cc15f37a..722f7f425 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -75,8 +75,6 @@ jobs: distribution: temurin java-version: 17 check-latest: true - - name: Generate Readme - run: sbt docs/generateReadme - name: Commit Changes run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" diff --git a/README.md b/README.md index 571bc9cf2..1018ffe9c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [ZIO Json](https://github.com/zio/zio-json) is a fast and secure JSON library with tight ZIO integration. -[![Production Ready](https://img.shields.io/badge/Project%20Stage-Production%20Ready-brightgreen.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-json/workflows/CI/badge.svg) [![Sonatype Releases](https://img.shields.io/nexus/r/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Release)](https://oss.sonatype.org/content/repositories/releases/dev/zio/zio-json_2.13/) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-json_2.13/) [![javadoc](https://javadoc.io/badge2/dev.zio/zio-json-docs_2.13/javadoc.svg)](https://javadoc.io/doc/dev.zio/zio-json-docs_2.13) [![ZIO JSON](https://img.shields.io/github/stars/zio/zio-json?style=social)](https://github.com/zio/zio-json) +[![Production Ready](https://img.shields.io/badge/Project%20Stage-Production%20Ready-brightgreen.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-json/workflows/CI/badge.svg) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-json_2.13/) [![ZIO JSON](https://img.shields.io/github/stars/zio/zio-json?style=social)](https://github.com/zio/zio-json) ## Introduction @@ -25,7 +25,7 @@ The goal of this project is to create the best all-round JSON library for Scala: In order to use this library, we need to add the following line in our `build.sbt` file: ```scala -libraryDependencies += "dev.zio" %% "zio-json" % "0.6.2" +libraryDependencies += "dev.zio" %% "zio-json" % "" ``` ## Example @@ -149,7 +149,7 @@ List(Apple(false), Banana(0.4)).toJsonPretty # How -Extreme **performance** is achieved by decoding JSON directly from the input source into business objects (docs/inspired by [plokhotnyuk](https://github.com/plokhotnyuk/jsoniter-scala)). Although not a requirement, the latest advances in [Java Loom](https://wiki.openjdk.java.net/display/loom/Main) can be used to support arbitrarily large payloads with near-zero overhead. +Extreme **performance** is achieved by decoding JSON directly from the input source into business objects (inspired by [plokhotnyuk](https://github.com/plokhotnyuk/jsoniter-scala)). Although not a requirement, the latest advances in [Java Loom](https://wiki.openjdk.java.net/display/loom/Main) can be used to support arbitrarily large payloads with near-zero overhead. Best in class **security** is achieved with an aggressive *early exit* strategy that avoids costly stack traces, even when parsing malformed numbers. Malicious (and badly formed) payloads are rejected before finishing reading. @@ -165,11 +165,11 @@ Learn more on the [ZIO JSON homepage](https://zio.dev/zio-json/)! ## Contributing -For the general guidelines, see ZIO [contributor's guide](https://zio.dev/about/contributing). +For the general guidelines, see ZIO [contributor's guide](https://zio.dev/contributor-guidelines). ## Code of Conduct -See the [Code of Conduct](https://zio.dev/about/code-of-conduct) +See the [Code of Conduct](https://zio.dev/code-of-conduct) ## Support diff --git a/build.sbt b/build.sbt index ec2eff059..aa577ae1f 100644 --- a/build.sbt +++ b/build.sbt @@ -400,7 +400,6 @@ lazy val docs = project zioJsonInteropScalaz7x.jvm, zioJsonGolden ), - docsPublishBranch := "series/2.x", readmeAcknowledgement := """|- Uses [JsonTestSuite](https://github.com/nst/JSONTestSuite) to test parsing. (c) 2016 Nicolas Seriot) | diff --git a/project/plugins.sbt b/project/plugins.sbt index afe456fcf..458f709e5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -10,6 +10,6 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") -addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.3.10") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.25") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.7" From 7f5ed7e8f4d88e1ce435c46c9b11d79e1b3d855a Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Mon, 13 May 2024 10:45:32 +0200 Subject: [PATCH 021/311] Ways to configure the explicit encoding of empty options as null (#1085) (#1100) * Ways to configure the explicit encoding of empty options as null (#1085) --- .github/workflows/site.yml | 4 +--- README.md | 4 ++-- .../src/main/scala-2.x/zio/json/macros.scala | 16 ++++++++++++---- .../src/main/scala-3/zio/json/macros.scala | 11 +++++++++-- .../zio/json/ConfigurableDeriveCodecSpec.scala | 17 +++++++++++++++++ .../src/test/scala/zio/json/EncoderSpec.scala | 10 ++++++++++ 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 722f7f425..73e43e749 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -1,4 +1,4 @@ -# This file was autogenerated using `zio-sbt-website` via `sbt generateGithubWorkflow` +# This file was autogenerated using `zio-sbt-website` via `sbt generateGithubWorkflow` # task and should be included in the git repository. Please do not edit it manually. name: Website @@ -29,8 +29,6 @@ jobs: check-latest: true - name: Check if the README file is up to date run: sbt docs/checkReadme - - name: Check if the site workflow is up to date - run: sbt docs/checkGithubWorkflow - name: Check artifacts build process run: sbt +publishLocal - name: Check website build process diff --git a/README.md b/README.md index 1018ffe9c..171044685 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [ZIO Json](https://github.com/zio/zio-json) is a fast and secure JSON library with tight ZIO integration. -[![Production Ready](https://img.shields.io/badge/Project%20Stage-Production%20Ready-brightgreen.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-json/workflows/CI/badge.svg) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-json_2.13/) [![ZIO JSON](https://img.shields.io/github/stars/zio/zio-json?style=social)](https://github.com/zio/zio-json) +[![Production Ready](https://img.shields.io/badge/Project%20Stage-Production%20Ready-brightgreen.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-json/workflows/CI/badge.svg) [![Sonatype Releases](https://img.shields.io/nexus/r/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Release)](https://oss.sonatype.org/content/repositories/releases/dev/zio/zio-json_2.13/) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-json_2.13/) [![javadoc](https://javadoc.io/badge2/dev.zio/zio-json-docs_2.13/javadoc.svg)](https://javadoc.io/doc/dev.zio/zio-json-docs_2.13) [![ZIO JSON](https://img.shields.io/github/stars/zio/zio-json?style=social)](https://github.com/zio/zio-json) ## Introduction @@ -25,7 +25,7 @@ The goal of this project is to create the best all-round JSON library for Scala: In order to use this library, we need to add the following line in our `build.sbt` file: ```scala -libraryDependencies += "dev.zio" %% "zio-json" % "" +libraryDependencies += "dev.zio" %% "zio-json" % "0.6.2" ``` ## Example diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 823687d29..42fffbb92 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -22,6 +22,8 @@ final case class jsonField(name: String) extends Annotation */ final case class jsonAliases(alias: String, aliases: String*) extends Annotation +final class jsonExplicitNull extends Annotation + /** * If used on a sealed class, will determine the name of the field for * disambiguating classes. @@ -212,7 +214,8 @@ final case class JsonCodecConfiguration( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, fieldNameMapping: JsonMemberFormat = IdentityFormat, allowExtraFields: Boolean = true, - sumTypeMapping: JsonMemberFormat = IdentityFormat + sumTypeMapping: JsonMemberFormat = IdentityFormat, + explicitNulls: Boolean = false ) object JsonCodecConfiguration { @@ -554,6 +557,10 @@ object DeriveJsonEncoder { name }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) } + + val explicitNulls: Boolean = + config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + lazy val tcs: Array[JsonEncoder[Any]] = params.map(p => p.typeclass.asInstanceOf[JsonEncoder[Any]]) val len: Int = params.length def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { @@ -564,9 +571,10 @@ object DeriveJsonEncoder { var prevFields = false // whether any fields have been written while (i < len) { - val tc = tcs(i) - val p = params(i).dereference(a) - if (!tc.isNothing(p)) { + val tc = tcs(i) + val p = params(i).dereference(a) + val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) + if (!tc.isNothing(p) || writeNulls) { // if we have at least one field already, we need a comma if (prevFields) { if (indent.isEmpty) out.write(",") diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index d61500909..301a02428 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -27,6 +27,11 @@ final case class jsonField(name: String) extends Annotation */ final case class jsonAliases(alias: String, aliases: String*) extends Annotation +/** + * Empty option fields will be encoded as `null`. + */ +final class jsonExplicitNull extends Annotation + /** * If used on a sealed class, will determine the name of the field for * disambiguating classes. @@ -540,6 +545,8 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => }) .toArray + val explicitNulls = ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + lazy val tcs: Array[JsonEncoder[Any]] = IArray.genericWrapArray(params.map(_.typeclass.asInstanceOf[JsonEncoder[Any]])).toArray @@ -555,8 +562,8 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => while (i < len) { val tc = tcs(i) val p = params(i).deref(a) - - if (! tc.isNothing(p)) { + val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) + if (! tc.isNothing(p) || writeNulls) { // if we have at least one field already, we need a comma if (prevFields) { if (indent.isEmpty) { diff --git a/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala index 4f2094c07..2cafc819f 100644 --- a/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala +++ b/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala @@ -14,6 +14,8 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { case class CaseClass(i: Int) extends ST } + case class OptionalField(a: Option[Int]) + def spec = suite("ConfigurableDeriveCodecSpec")( suite("defaults")( suite("string")( @@ -177,6 +179,21 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { ) } ) + ), + suite("explicit nulls")( + test("write null if configured") { + val expectedStr = """{"a":null}""" + val expectedObj = OptionalField(None) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitNulls = true) + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + } ) ) } diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index c8cd85791..f8b895ec1 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -291,6 +291,7 @@ object EncoderSpec extends ZIOSpecDefault { ) && assert(CoupleOfThings(0, None, true).toJsonPretty)(equalTo("{\n \"j\" : 0,\n \"b\" : true\n}")) && assert(OptionalAndRequired(None, "foo").toJson)(equalTo("""{"s":"foo"}""")) + assert(OptionalExplicitNullAndRequired(None, "foo").toJson)(equalTo("""{"i":null,"s":"foo"}""")) }, test("sum encoding") { import examplesum._ @@ -468,6 +469,15 @@ object EncoderSpec extends ZIOSpecDefault { DeriveJsonEncoder.gen[OptionalAndRequired] } + @jsonExplicitNull + case class OptionalExplicitNullAndRequired(i: Option[Int], s: String) + + object OptionalExplicitNullAndRequired { + + implicit val encoder: JsonEncoder[OptionalExplicitNullAndRequired] = + DeriveJsonEncoder.gen[OptionalExplicitNullAndRequired] + } + case class Aliases(@jsonAliases("j", "k") i: Int, f: String) object Aliases { From 3d682cf573830d2393b6457567a54100dc768d26 Mon Sep 17 00:00:00 2001 From: Alexander Savchuk <38427413+amsavchuk@users.noreply.github.com> Date: Mon, 13 May 2024 13:39:25 +0400 Subject: [PATCH 022/311] JsonCursor composition (#1110) --- .../main/scala/zio/json/ast/JsonCursor.scala | 16 +++++++------ .../test/scala/zio/json/ast/JsonSpec.scala | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/JsonCursor.scala b/zio-json/shared/src/main/scala/zio/json/ast/JsonCursor.scala index 1a5e09012..7788a35c4 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/JsonCursor.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/JsonCursor.scala @@ -19,18 +19,20 @@ sealed trait JsonCursor[-From, +To <: Json] { self => final def >>>[Next <: Json](that: JsonCursor[To, Next]): JsonCursor[From, Next] = (that.asInstanceOf[JsonCursor[_ <: Json, _ <: Json]] match { case JsonCursor.Identity => - that + self - case JsonCursor.DownField(oldParent @ _, name) => - JsonCursor.DownField(self.asInstanceOf[JsonCursor[Json, Json.Obj]], name) + case JsonCursor.DownField(oldParent: JsonCursor[To, Json.Obj], name) => + JsonCursor.DownField(self >>> oldParent, name) - case JsonCursor.DownElement(oldParent @ _, index) => - JsonCursor.DownElement(self.asInstanceOf[JsonCursor[Json, Json.Arr]], index) + case JsonCursor.DownElement(oldParent: JsonCursor[To, Json.Arr], index) => + JsonCursor.DownElement(self >>> oldParent, index) - case JsonCursor.FilterType(oldParent @ _, tpe) => - JsonCursor.FilterType(self.asInstanceOf[JsonCursor[Json, Json]], tpe) + case JsonCursor.FilterType(oldParent: JsonCursor[To, _], tpe) => + JsonCursor.FilterType(self >>> oldParent, tpe) }).asInstanceOf[JsonCursor[From, Next]] + final def andThen[Next <: Json](that: JsonCursor[To, Next]): JsonCursor[From, Next] = self >>> that + final def isArray: JsonCursor[Json, Json.Arr] = filterType(JsonType.Arr) final def isBool: JsonCursor[Json, Json.Bool] = filterType(JsonType.Bool) diff --git a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala index ab50390b2..996a5b847 100644 --- a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala @@ -1,5 +1,6 @@ package zio.json.ast +import zio.json._ import zio.test.Assertion._ import zio.test._ @@ -243,6 +244,29 @@ object JsonSpec extends ZIOSpecDefault { assert(tweet.get(combined))( isRight(equalTo(Json.Str("twitter"))) ) + }, + test(">>>, array, filterType (second operand of >>> is complex)") { + val downEntities = JsonCursor.field("entities") + val downHashtag = + JsonCursor.isObject >>> JsonCursor.field("hashtags") >>> JsonCursor.isArray >>> JsonCursor.element(0) + + val combined = downEntities >>> downHashtag + + assert(tweet.get(combined))( + isRight(equalTo(Json.Str("twitter"))) + ) + }, + test(">>>, combination of some methods of JsonCursor (second operand of >>> is complex)") { + val posts: Json = """{"posts": [{"id": 0, "title": "foo"}]}""".fromJson[Json].toOption.get + + val downPosts = JsonCursor.field("posts") + val downTitle = JsonCursor.isArray >>> JsonCursor.element(0) >>> JsonCursor.isObject >>> + JsonCursor.field("title") >>> JsonCursor.isString + val combined = downPosts >>> downTitle + + assert(posts.get(combined))( + isRight(equalTo(Json.Str("foo"))) + ) } ), suite("intersect")( From a1c964e6bc4f72c44db4b311d63649b2b40c35b9 Mon Sep 17 00:00:00 2001 From: Alexander Savchuk <38427413+amsavchuk@users.noreply.github.com> Date: Tue, 14 May 2024 03:00:56 +0400 Subject: [PATCH 023/311] encodeJsonArrayPipeline produces incorrect JSON when stream is empty (#1114) --- .../zio/json/JsonEncoderPlatformSpecific.scala | 17 ++++++++++------- .../zio/json/EncoderPlatformSpecificSpec.scala | 10 +++++++++- .../src/test/scala/zio/json/ast/JsonSpec.scala | 10 ++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala b/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala index 516fc41fd..3502c7270 100644 --- a/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala +++ b/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala @@ -43,17 +43,20 @@ trait JsonEncoderPlatformSpecific[A] { self: JsonEncoder[A] => ) } } - writeWriter <- ZIO.succeed(new WriteWriter(writer)) + writeWriter <- ZIO.succeed(new WriteWriter(writer)) + hasAtLeastOneElement <- Ref.make(false) push = { (is: Option[Chunk[A]]) => val pushChars = chunkBuffer.getAndUpdate(c => if (c.isEmpty) c else Chunk()) is match { case None => - ZIO.attemptBlocking(writer.close()) *> pushChars.map { terminal => - endWith.fold(terminal) { last => - // Chop off terminal delimiter - (if (delimiter.isDefined) terminal.dropRight(1) else terminal) :+ last - } + ZIO.attemptBlocking(writer.close()) *> pushChars.flatMap { terminal => + hasAtLeastOneElement.get.map(nonEmptyStream => + endWith.fold(terminal) { last => + // Chop off terminal delimiter if stream is not empty + (if (delimiter.isDefined && nonEmptyStream) terminal.dropRight(1) else terminal) :+ last + } + ) } case Some(xs) => @@ -64,7 +67,7 @@ trait JsonEncoderPlatformSpecific[A] { self: JsonEncoder[A] => for (s <- delimiter) writeWriter.write(s) } - } *> pushChars + } *> hasAtLeastOneElement.set(true).when(xs.nonEmpty) *> pushChars } } } yield push diff --git a/zio-json/jvm/src/test/scala-2/zio/json/EncoderPlatformSpecificSpec.scala b/zio-json/jvm/src/test/scala-2/zio/json/EncoderPlatformSpecificSpec.scala index ba37179d5..7479e378b 100644 --- a/zio-json/jvm/src/test/scala-2/zio/json/EncoderPlatformSpecificSpec.scala +++ b/zio-json/jvm/src/test/scala-2/zio/json/EncoderPlatformSpecificSpec.scala @@ -7,7 +7,7 @@ import testzio.json.data.googlemaps._ import testzio.json.data.twitter._ import zio.Chunk import zio.json.ast.Json -import zio.stream.ZStream +import zio.stream.{ ZSink, ZStream } import zio.test.Assertion._ import zio.test.{ ZIOSpecDefault, assert, _ } @@ -75,6 +75,14 @@ object EncoderPlatformSpecificSpec extends ZIOSpecDefault { } yield { assert(xs.mkString)(equalTo("""[{"id":1},{"id":2},{"id":3}]""")) } + }, + test("encodeJsonArrayPipeline, empty stream") { + val emptyArray = ZStream + .from(List()) + .via(JsonEncoder[String].encodeJsonArrayPipeline) + .run(ZSink.mkString) + + assertZIO(emptyArray)(equalTo("[]")) } ), suite("helpers in zio.json")( diff --git a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala index 996a5b847..069f16594 100644 --- a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala @@ -267,6 +267,16 @@ object JsonSpec extends ZIOSpecDefault { assert(posts.get(combined))( isRight(equalTo(Json.Str("foo"))) ) + }, + test(">>>, identity") { + val obj = Json.Obj("a" -> Json.Num(1)) + + val fieldA = JsonCursor.field("a") + val identity = JsonCursor.identity + + val num = obj.get(fieldA >>> identity) + + assert(num)(isRight(equalTo(Json.Num(1)))) } ), suite("intersect")( From 8ea1220f3dfad8647f6bce98830b93af58b464a6 Mon Sep 17 00:00:00 2001 From: Jisoo Park Date: Thu, 16 May 2024 03:02:22 +0900 Subject: [PATCH 024/311] Add `JsonCodec` for AST (#1116) --- .../shared/src/main/scala/zio/json/ast/ast.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index 5d2f1f900..694d95ae1 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -391,6 +391,8 @@ object Json { override final def toJsonAST(a: Obj): Either[String, Json] = Right(a) } + + implicit val codec: JsonCodec[Obj] = JsonCodec(encoder, decoder) } final case class Arr(elements: Chunk[Json]) extends Json { def isEmpty: Boolean = elements.isEmpty @@ -434,6 +436,8 @@ object Json { override final def toJsonAST(a: Arr): Either[String, Json] = Right(a) } + + implicit val codec: JsonCodec[Arr] = JsonCodec(encoder, decoder) } final case class Bool(value: Boolean) extends Json { override def asBoolean: Some[Boolean] = Some(value) @@ -460,6 +464,8 @@ object Json { override final def toJsonAST(a: Bool): Either[String, Json] = Right(a) } + + implicit val codec: JsonCodec[Bool] = JsonCodec(encoder, decoder) } final case class Str(value: String) extends Json { override def asString: Some[String] = Some(value) @@ -482,6 +488,8 @@ object Json { override final def toJsonAST(a: Str): Either[String, Json] = Right(a) } + + implicit val codec: JsonCodec[Str] = JsonCodec(encoder, decoder) } final case class Num(value: java.math.BigDecimal) extends Json { override def asNumber: Some[Json.Num] = Some(this) @@ -512,6 +520,8 @@ object Json { override final def toJsonAST(a: Num): Either[String, Num] = Right(a) } + + implicit val codec: JsonCodec[Num] = JsonCodec(encoder, decoder) } type Null = Null.type case object Null extends Json { @@ -535,6 +545,8 @@ object Json { override final def toJsonAST(a: Null.type): Either[String, Json] = Right(a) } + implicit val codec: JsonCodec[Null.type] = JsonCodec(encoder, decoder) + override def asNull: Some[Unit] = Some(()) } @@ -572,5 +584,7 @@ object Json { override final def toJsonAST(a: Json): Either[String, Json] = Right(a) } + implicit val codec: JsonCodec[Json] = JsonCodec(encoder, decoder) + def apply(fields: (String, Json)*): Json = Json.Obj(Chunk(fields: _*)) } From 86170606ee6e1b1add7790499c99378d0b7b26a1 Mon Sep 17 00:00:00 2001 From: Jisoo Park Date: Fri, 17 May 2024 21:04:55 +0900 Subject: [PATCH 025/311] Add JsonCodec derivation from encoder and decoder (#1115) * Add JsonCodec derivation from encoder and decoder * Disable JsonCodec derivaton from encoder & decoder for Scala 2.12 --- .../scala-2.12/zio/json/JsonCodecVersionSpecific.scala | 3 +++ .../scala-2.13/zio/json/JsonCodecVersionSpecific.scala | 7 +++++++ .../main/scala-2.x/zio/json/JsonCodecVersionSpecific.scala | 3 --- .../scala-2.x/zio/json/JsonDecoderVersionSpecific.scala | 2 +- .../scala-2.x/zio/json/JsonEncoderVersionSpecific.scala | 2 +- .../main/scala-3/zio/json/JsonCodecVersionSpecific.scala | 5 ++++- .../main/scala-3/zio/json/JsonDecoderVersionSpecific.scala | 2 +- .../main/scala-3/zio/json/JsonEncoderVersionSpecific.scala | 2 +- zio-json/shared/src/main/scala/zio/json/JsonCodec.scala | 2 -- 9 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala create mode 100644 zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala delete mode 100644 zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecVersionSpecific.scala diff --git a/zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala new file mode 100644 index 000000000..e57d1dc36 --- /dev/null +++ b/zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala @@ -0,0 +1,3 @@ +package zio.json + +private[json] trait JsonCodecVersionSpecific diff --git a/zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala new file mode 100644 index 000000000..f3292ed19 --- /dev/null +++ b/zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala @@ -0,0 +1,7 @@ +package zio.json + +private[json] trait JsonCodecVersionSpecific { + + implicit def fromEncoderDecoder[A](implicit encoder: JsonEncoder[A], decoder: JsonDecoder[A]): JsonCodec[A] = + JsonCodec(encoder, decoder) +} diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecVersionSpecific.scala deleted file mode 100644 index cfe9b163e..000000000 --- a/zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecVersionSpecific.scala +++ /dev/null @@ -1,3 +0,0 @@ -package zio.json - -trait JsonCodecVersionSpecific diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala index 728b65002..1a3e5e1f3 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala @@ -1,3 +1,3 @@ package zio.json -trait JsonDecoderVersionSpecific +private[json] trait JsonDecoderVersionSpecific diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala index 5b912efe2..f9508efa0 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala @@ -1,3 +1,3 @@ package zio.json -trait JsonEncoderVersionSpecific +private[json] trait JsonEncoderVersionSpecific diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala index 44bd2673d..9b0319709 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala @@ -1,5 +1,8 @@ package zio.json -trait JsonCodecVersionSpecific { +private[json] trait JsonCodecVersionSpecific { inline def derived[A: deriving.Mirror.Of]: JsonCodec[A] = DeriveJsonCodec.gen[A] + + given fromEncoderDecoder[A](using encoder: JsonEncoder[A], decoder: JsonDecoder[A]): JsonCodec[A] = + JsonCodec(encoder, decoder) } diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala index 233e6ac9a..5cb9df085 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala @@ -1,5 +1,5 @@ package zio.json -trait JsonDecoderVersionSpecific { +private[json] trait JsonDecoderVersionSpecific { inline def derived[A: deriving.Mirror.Of]: JsonDecoder[A] = DeriveJsonDecoder.gen[A] } diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala index 7cd37a803..09a5f91cd 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala @@ -1,5 +1,5 @@ package zio.json -trait JsonEncoderVersionSpecific { +private[json] trait JsonEncoderVersionSpecific { inline def derived[A: deriving.Mirror.Of]: JsonEncoder[A] = DeriveJsonEncoder.gen[A] } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala index 9de0ac4bb..fdad66d18 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala @@ -87,8 +87,6 @@ final case class JsonCodec[A](encoder: JsonEncoder[A], decoder: JsonDecoder[A]) object JsonCodec extends GeneratedTupleCodecs with CodecLowPriority0 with JsonCodecVersionSpecific { def apply[A](implicit jsonCodec: JsonCodec[A]): JsonCodec[A] = jsonCodec - def apply[A](encoder: JsonEncoder[A], decoder: JsonDecoder[A]): JsonCodec[A] = new JsonCodec(encoder, decoder) - private def orElseEither[A, B](A: JsonCodec[A], B: JsonCodec[B]): JsonCodec[Either[A, B]] = JsonCodec( JsonEncoder.orElseEither[A, B](A.encoder, B.encoder), From 1ca30deb1031e6dd832c6fe7e8ffec15a43b6c85 Mon Sep 17 00:00:00 2001 From: Milad Khajavi Date: Fri, 17 May 2024 15:51:30 +0330 Subject: [PATCH 026/311] checking if the readme is up-to-date is deprecated. (#1117) --- .github/workflows/site.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 73e43e749..01cbaa3df 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -27,8 +27,6 @@ jobs: distribution: temurin java-version: 17 check-latest: true - - name: Check if the README file is up to date - run: sbt docs/checkReadme - name: Check artifacts build process run: sbt +publishLocal - name: Check website build process From 5a4ce31c2d83c0d6126b458e44404138893343e0 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 30 May 2024 18:12:23 +0200 Subject: [PATCH 027/311] Update zio-sbt-website to 0.4.0-alpha.27 (#1120) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 458f709e5..a68356100 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -10,6 +10,6 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") -addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.25") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.27") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.7" From a45f620939256145342b8afd2cc2e139e926c2ee Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 30 May 2024 20:34:47 +0200 Subject: [PATCH 028/311] Update jawn-ast to 1.6.0 (#1122) --- build.sbt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index aa577ae1f..aa0db84ec 100644 --- a/build.sbt +++ b/build.sbt @@ -227,7 +227,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Vector( - "org.typelevel" %% "jawn-ast" % "1.5.1" % "test" + "org.typelevel" %% "jawn-ast" % "1.6.0" % "test" ) case Some((2, n)) => @@ -235,13 +235,13 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) Seq( "com.particeep" %% "play-json-extensions" % "0.43.1" % "test", "com.typesafe.play" %%% "play-json" % "2.9.4" % "test", - "org.typelevel" %% "jawn-ast" % "1.5.1" % "test" + "org.typelevel" %% "jawn-ast" % "1.6.0" % "test" ) } else { Seq( "ai.x" %% "play-json-extensions" % "0.42.0" % "test", "com.typesafe.play" %%% "play-json" % "2.9.4" % "test", - "org.typelevel" %% "jawn-ast" % "1.5.1" % "test" + "org.typelevel" %% "jawn-ast" % "1.6.0" % "test" ) } From 6e7079c8e5f08fa7ec2b1c5b463c031696bd1af7 Mon Sep 17 00:00:00 2001 From: Balduin Landolt <33053745+BalduinLandolt@users.noreply.github.com> Date: Mon, 3 Jun 2024 18:52:06 +0200 Subject: [PATCH 029/311] docs: Add documentation on AST and Cursor (#1098) Co-authored-by: Milad Khajavi --- docs/decoding.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/docs/decoding.md b/docs/decoding.md index 5a72f12c5..eaabaecf7 100644 --- a/docs/decoding.md +++ b/docs/decoding.md @@ -360,3 +360,107 @@ And now, the Json decoder for Animal can handle both formats: """{"name": "Snake", "categories": [ "Cold-blooded", "Reptile"]}""".fromJson[Animal] // >> Right(Animal(Snake,List(Cold-blooded, Reptile))) ``` + +# JSON AST and Cursors + +In most cases it is not necessary to work with the JSON AST directly, +instead it is more convenient to decode directly to domain objects. +However, sometimes it is handy to work with a lower level representation of JSON. +This may for example be the case when you need to work with deeply nested JSON structures +that would result in deeply nested case classes, +or when you expect a lot of variation in the JSON structure, which would result in nasty decoders. + + +## JSON AST + +To get the AST representation of a JSON string, use the `fromJson[Json]` method. + +```scala mdoc +import zio.json._ +import zio.json.ast._ + +val jsonString: String = """{"name": "John Doe"}""" +val jsonAst: Either[String, Json] = jsonString.fromJson[Json] +``` + +The `Json` type is a recursive data structure that can be navigated in a fairly straightforward way. + +```scala mdoc:reset + +import zio.Chunk +import zio.json._ +import zio.json.ast.Json +import zio.json.ast.Json._ + +val jsonString: String = """{"name": "John Doe"}""" +val jsonAst: Json = jsonString.fromJson[Json].toOption.get +jsonAst match { + case Obj(fields: Chunk[(String, Json)]) => () + case Arr(elements: Chunk[Json]) => () + case Bool(value: Boolean) => () + case Str(value: String) => () + case Num(value: java.math.BigDecimal) => () + case Json.Null => () +} +``` + +To get the `name` field, you could do the following: + +```scala mdoc +import zio.json._ +import zio.json.ast.Json + +val json: Option[Json] = """{"name": "John Doe"}""".fromJson[Json].toOption +val name: Option[String] = json.flatMap { json => + json match { + case Json.Obj(fields) => fields.collectFirst { case ("name", Json.Str(name)) => name } + case _ => None + } +} +``` + +## Cursors + +In practice, it is normally more convenient to use cursors to navigate the JSON AST. + +```scala mdoc:reset +import zio.json._ +import zio.json.ast.Json +import zio.json.ast.JsonCursor +import zio.json.ast.Json.Str + +val json: Either[String, Json] = """{"name": "John Doe"}""".fromJson[Json] +val cursor: JsonCursor[Json, Str] = JsonCursor.field("name").isString +val name: Either[String, String] = json.flatMap(_.get(cursor).map(_.value)) +``` + +Cursors can be composed to navigate more complex JSON structures. + +```scala mdoc +import zio.json._ +import zio.json.ast.Json +import zio.json.ast.JsonCursor + +val json1: Either[String, Json] = """{"posts": [{"id": 0, "title": "foo"}]}""".fromJson[Json] +val json2: Either[String, Json] = """{"userPosts": [{"id": 1, "title": "bar"}]}""".fromJson[Json] + +val commonCursor = + JsonCursor.isArray >>> + JsonCursor.element(0) >>> + JsonCursor.isObject >>> + JsonCursor.field("title") >>> + JsonCursor.isString + +val cursor1 = JsonCursor.field("posts") +val cursor2 = JsonCursor.field("userPosts") + +def getTitle(json: Either[String, Json]) = + for { + ast <- json + posts <- ast.get(cursor1).orElse(ast.get(cursor2)) + title <- posts.get(commonCursor).map(_.value) + } yield title + +val title1 = getTitle(json1) +val title2 = getTitle(json2) +``` From df9c1474e741b6928c72632c04411e04f62d610c Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 13 Jun 2024 20:13:33 +0200 Subject: [PATCH 030/311] Update scala-java-time-tzdb to 2.6.0 (#1124) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index aa0db84ec..08edd179e 100644 --- a/build.sbt +++ b/build.sbt @@ -219,7 +219,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .jsSettings( libraryDependencies ++= Seq( "io.github.cquiroz" %%% "scala-java-time" % "2.5.0", - "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.5.0" + "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.6.0" ) ) .jvmSettings( From 608bb6e1e5c213ec53a6258f3cc30819cac5f07f Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Fri, 14 Jun 2024 01:33:36 +0200 Subject: [PATCH 031/311] feat: implement Scala 3 Constant and Union support for string-based literals as enums (#1125) * feat: implement Scala 3 Constant and Union support for string-based literals as enums * add JsonCodec test * format * add non-mdoc examples (needs Scala 3) --- docs/decoding.md | 16 ++++ .../zio/json/JsonDecoderVersionSpecific.scala | 2 + .../zio/json/JsonEncoderVersionSpecific.scala | 2 + .../zio/json/JsonDecoderVersionSpecific.scala | 23 ++++++ .../zio/json/JsonEncoderVersionSpecific.scala | 11 +++ .../scala-3/zio/json/union_derivation.scala | 74 +++++++++++++++++++ .../src/main/scala/zio/json/JsonDecoder.scala | 2 +- .../src/main/scala/zio/json/JsonEncoder.scala | 2 +- .../scala-3/zio/json/DerivedCodecSpec.scala | 7 +- .../scala-3/zio/json/DerivedDecoderSpec.scala | 6 ++ .../scala-3/zio/json/DerivedEncoderSpec.scala | 5 ++ 11 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala diff --git a/docs/decoding.md b/docs/decoding.md index eaabaecf7..7d43c1ff9 100644 --- a/docs/decoding.md +++ b/docs/decoding.md @@ -83,6 +83,22 @@ object Fruit { """{ "Apple": { "poison": false }}""".fromJson[Fruit] ``` +### String-based union types (Enum) +The codecs support string-based union types (enums) out of the box. This is useful when the overhead of a Enum is not desired. + +```scala +val appleOrBanana: "Apple" | "Banana" = "Apple" +``` +Decoding succeeds because 'Apple' is a valid value +```scala +appleOrBanana.toJson +"Apple".fromJson["Apple" | "Banana"] +``` +Decoding fail because 'Pear' is not a valid value +```scala +"Peer".fromJson["Apple" | "Banana"] +``` + Almost all of the standard library data types are supported as fields on the case class, and it is easy to add support if one is missing. ## Manual instances diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala index 1a3e5e1f3..5e8d10030 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala @@ -1,3 +1,5 @@ package zio.json private[json] trait JsonDecoderVersionSpecific + +private[json] trait DecoderLowPriorityVersionSpecific diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala index f9508efa0..8b360d95d 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala @@ -1,3 +1,5 @@ package zio.json private[json] trait JsonEncoderVersionSpecific + +private[json] trait EncoderLowPriorityVersionSpecific diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala index 5cb9df085..64f51eaee 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala @@ -1,5 +1,28 @@ package zio.json +import scala.compiletime.* +import scala.compiletime.ops.any.IsConst + private[json] trait JsonDecoderVersionSpecific { inline def derived[A: deriving.Mirror.Of]: JsonDecoder[A] = DeriveJsonDecoder.gen[A] } + +trait DecoderLowPriorityVersionSpecific { + + inline given unionOfStringEnumeration[T](using IsUnionOf[String, T]): JsonDecoder[T] = + val values = UnionDerivation.constValueUnionTuple[String, T] + JsonDecoder.string.mapOrFail( + { + case raw if values.toList.contains(raw) => Right(raw.asInstanceOf[T]) + case _ => Left("expected one of: " + values.toList.mkString(", ")) + } + ) + + inline given constStringToEnum[T <: String](using IsConst[T] =:= true): JsonDecoder[T] = + JsonDecoder.string.mapOrFail( + { + case raw if raw == constValue[T] => Right(constValue[T]) + case _ => Left("expected one of: " + constValue[T]) + } + ) +} diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala index 09a5f91cd..97d623113 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala @@ -1,5 +1,16 @@ package zio.json +import scala.compiletime.ops.any.IsConst + private[json] trait JsonEncoderVersionSpecific { inline def derived[A: deriving.Mirror.Of]: JsonEncoder[A] = DeriveJsonEncoder.gen[A] } + +private[json] trait EncoderLowPriorityVersionSpecific { + + inline given unionOfStringEnumeration[T](using IsUnionOf[String, T]): JsonEncoder[T] = + JsonEncoder.string.asInstanceOf[JsonEncoder[T]] + + inline given constStringToEnum[T <: String](using IsConst[T] =:= true): JsonEncoder[T] = + JsonEncoder.string.narrow[T] +} diff --git a/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala new file mode 100644 index 000000000..0195b89c0 --- /dev/null +++ b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala @@ -0,0 +1,74 @@ +package zio.json + +import scala.compiletime.* +import scala.deriving.* +import scala.quoted.* + +@scala.annotation.implicitNotFound("${A} is not a union type") +sealed trait IsUnion[A] + +object IsUnion: + + private val singleton: IsUnion[Any] = new IsUnion[Any] {} + + transparent inline given derived[A]: IsUnion[A] = ${ deriveImpl[A] } + + private def deriveImpl[A](using quotes: Quotes, t: Type[A]): Expr[IsUnion[A]] = + import quotes.reflect.* + val tpe: TypeRepr = TypeRepr.of[A] + tpe.dealias match + case o: OrType => ('{ IsUnion.singleton.asInstanceOf[IsUnion[A]] }).asExprOf[IsUnion[A]] + case other => report.errorAndAbort(s"${tpe.show} is not a Union") + +@scala.annotation.implicitNotFound("${A} is not a union of ${T}") +sealed trait IsUnionOf[T, A] + +object IsUnionOf: + + private val singleton: IsUnionOf[Any, Any] = new IsUnionOf[Any, Any] {} + + transparent inline given derived[T, A]: IsUnionOf[T, A] = ${ deriveImpl[T, A] } + + private def deriveImpl[T, A](using quotes: Quotes, t: Type[T], a: Type[A]): Expr[IsUnionOf[T, A]] = + import quotes.reflect.* + val tpe: TypeRepr = TypeRepr.of[A] + val bound: TypeRepr = TypeRepr.of[T] + + def validateTypes(tpe: TypeRepr): Unit = + tpe.dealias match + case o: OrType => + validateTypes(o.left) + validateTypes(o.right) + case o => + if o <:< bound then () + else report.errorAndAbort(s"${o.show} is not a subtype of ${bound.show}") + + tpe.dealias match + case o: OrType => + validateTypes(o) + ('{ IsUnionOf.singleton.asInstanceOf[IsUnionOf[T, A]] }).asExprOf[IsUnionOf[T, A]] + case other => report.errorAndAbort(s"${tpe.show} is not a Union") + +object UnionDerivation: + transparent inline def constValueUnionTuple[T, A](using IsUnionOf[T, A]): Tuple = ${ constValueUnionTupleImpl[T, A] } + + private def constValueUnionTupleImpl[T: Type, A: Type](using Quotes): Expr[Tuple] = + Expr.ofTupleFromSeq(constTypes[T, A]) + + private def constTypes[T: Type, A: Type](using Quotes): List[Expr[Any]] = + import quotes.reflect.* + val tpe: TypeRepr = TypeRepr.of[A] + val bound: TypeRepr = TypeRepr.of[T] + + def transformTypes(tpe: TypeRepr): List[TypeRepr] = + tpe.dealias match + case o: OrType => + transformTypes(o.left) ::: transformTypes(o.right) + case o: Constant if o <:< bound && o.isSingleton => + o :: Nil + case o => + report.errorAndAbort(s"${o.show} is not a subtype of ${bound.show}") + + transformTypes(tpe).distinct.map(_.asType match + case '[t] => '{ constValue[t] } + ) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 2d4b26507..ddf45c725 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -699,6 +699,6 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } -private[json] trait DecoderLowPriority4 { +private[json] trait DecoderLowPriority4 extends DecoderLowPriorityVersionSpecific { implicit def fromCodec[A](implicit codec: JsonCodec[A]): JsonDecoder[A] = codec.decoder } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index cb2882b56..6b1dc40e6 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -520,6 +520,6 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { implicit val currency: JsonEncoder[java.util.Currency] = stringify(_.toString) } -private[json] trait EncoderLowPriority4 { +private[json] trait EncoderLowPriority4 extends EncoderLowPriorityVersionSpecific { implicit def fromCodec[A](implicit codec: JsonCodec[A]): JsonEncoder[A] = codec.encoder } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala index 7a15eb5ff..84d1f2313 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala @@ -27,6 +27,11 @@ object DerivedCodecSpec extends ZIOSpecDefault { (Foo.Qux(Foo.Bar): Foo).toJson.fromJson[Foo] """ })(isRight(anything)) - } + }, + test("Derives and encodes for a union of string-based literals") { + case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonCodec + + assertTrue(Foo("A", Some("A")).toJson.fromJson[Foo] == Right(Foo("A", Some("A")))) + }, ) } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala index a2d702e34..a1520753c 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala @@ -28,6 +28,12 @@ object DerivedDecoderSpec extends ZIOSpecDefault { "{\"Qux\":{\"foo\":{\"Bar\":{}}}}".fromJson[Foo] """ })(isRight(anything)) + }, + test("Derives and decodes for a union of string-based literals") { + case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonDecoder + + assertTrue("""{"aOrB": "A", "optA": "A"}""".fromJson[Foo] == Right(Foo("A", Some("A")))) && + assertTrue("""{"aOrB": "C"}""".fromJson[Foo] == Left(".aOrB(expected one of: A, B)")) } ) } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala index 20fd42888..9b7fc862d 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala @@ -27,6 +27,11 @@ object DerivedEncoderSpec extends ZIOSpecDefault { (Foo.Qux(Foo.Bar): Foo).toJson """ })(isRight(anything)) + }, + test("Derives and encodes for a union of string-based literals") { + case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonEncoder + + assertTrue(Foo("A", Some("A")).toJson == """{"aOrB":"A","optA":"A"}""") } ) } From 5773f6c0c6fc970a539e4735e3fb018c18d54752 Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:30:37 +0200 Subject: [PATCH 032/311] Feat: scala3 enumeration support (#1068) * addscala 3 enumeration decoding/encoding --- docs/decoding.md | 17 +++ docs/encoding.md | 17 +++ .../scala/zio/json/golden/GoldenSpec.scala | 6 +- .../src/main/scala-3/zio/json/macros.scala | 112 ++++++++++++++---- .../scala-3/zio/json/DerivedDecoderSpec.scala | 51 +++++--- .../scala-3/zio/json/DerivedEncoderSpec.scala | 51 +++++--- 6 files changed, 193 insertions(+), 61 deletions(-) diff --git a/docs/decoding.md b/docs/decoding.md index 7d43c1ff9..cb8d7abb1 100644 --- a/docs/decoding.md +++ b/docs/decoding.md @@ -101,6 +101,23 @@ Decoding fail because 'Pear' is not a valid value Almost all of the standard library data types are supported as fields on the case class, and it is easy to add support if one is missing. +### Sealed families and enums for Scala 3 +Sealed families where all members are only objects, or a Scala 3 enum with all cases parameterless are interpreted as enumerations and will encode 1:1 with their value-names. +```scala +enum Foo derives JsonDecoder: + case Bar + case Baz + case Qux +``` +or +```scala +sealed trait Foo derives JsonDecoder +object Foo: + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo +``` + ## Manual instances Sometimes it is easier to reuse an existing `JsonDecoder` rather than generate a new one. This can be accomplished using convenience methods on the `JsonDecoder` typeclass to *derive* new decoders diff --git a/docs/encoding.md b/docs/encoding.md index b43123d80..6c9f92d4a 100644 --- a/docs/encoding.md +++ b/docs/encoding.md @@ -55,6 +55,23 @@ apple.toJson Almost all of the standard library data types are supported as fields on the case class, and it is easy to add support if one is missing. +### Sealed families and enums for Scala 3 +Sealed families where all members are only objects, or a Scala 3 enum with all cases parameterless are interpreted as enumerations and will encode 1:1 with their value-names. +```scala +enum Foo derives JsonEncoder: + case Bar + case Baz + case Qux +``` +or +```scala +sealed trait Foo derives JsonEncoder +object Foo: + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo +``` + ## Manual instances Sometimes it is easier to reuse an existing `JsonEncoder` rather than generate a new one. This can be accomplished using convenience methods on the `JsonEncoder` typeclass to *derive* new decoders: diff --git a/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala b/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala index 42547fc16..01b539033 100644 --- a/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala +++ b/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala @@ -11,9 +11,9 @@ object GoldenSpec extends ZIOSpecDefault { sealed trait SumType object SumType { - case object Case1 extends SumType - case object Case2 extends SumType - case object Case3 extends SumType + case object Case1 extends SumType + case object Case2 extends SumType + case class Case3() extends SumType implicit val jsonCodec: JsonCodec[SumType] = DeriveJsonCodec.gen } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 301a02428..3392d476a 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -207,21 +207,7 @@ final class jsonNoExtraFields extends Annotation */ final class jsonExclude extends Annotation -// TODO: implement same configuration as for Scala 2 once this issue is resolved: https://github.com/softwaremill/magnolia/issues/296 -object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => - def join[A](ctx: CaseClass[Typeclass, A]): JsonDecoder[A] = { - val (transformNames, nameTransform): (Boolean, String => String) = - ctx.annotations.collectFirst { case jsonMemberNames(format) => format } - .map(true -> _) - .getOrElse(false -> identity) - - val no_extra = ctx - .annotations - .collectFirst { case _: jsonNoExtraFields => () } - .isDefined - - if (ctx.params.isEmpty) { - new JsonDecoder[A] { +private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) extends JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { if (no_extra) { Lexer.char(trace, in, '{') @@ -239,6 +225,22 @@ object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) } } + +// TODO: implement same configuration as for Scala 2 once this issue is resolved: https://github.com/softwaremill/magnolia/issues/296 +object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => + def join[A](ctx: CaseClass[Typeclass, A]): JsonDecoder[A] = { + val (transformNames, nameTransform): (Boolean, String => String) = + ctx.annotations.collectFirst { case jsonMemberNames(format) => format } + .map(true -> _) + .getOrElse(false -> identity) + + val no_extra = ctx + .annotations + .collectFirst { case _: jsonNoExtraFields => () } + .isDefined + + if (ctx.params.isEmpty) { + new CaseObjectDecoder(ctx, no_extra) } else { new JsonDecoder[A] { val (names, aliases): (Array[String], Array[(String, Int)]) = { @@ -400,9 +402,35 @@ object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => lazy val namesMap: Map[String, Int] = names.zipWithIndex.toMap + def isEnumeration = + (ctx.isEnum && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[?, ?]])) || ( + !ctx.isEnum && ctx.subtypes.forall(_.isObject) + ) + def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n } - if (discrim.isEmpty) { + if (isEnumeration) { + new JsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val typeName = Lexer.string(trace, in).toString() + namesMap.find(_._1 == typeName) match { + case Some((_, idx)) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + case None => throw UnsafeJson(JsonError.Message(s"Invalid enumeration value $typeName") :: trace) + } + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = { + json match { + case Json.Str(typeName) => + ctx.subtypes.find(_.typeInfo.short == typeName) match { + case Some(sub) => sub.typeclass.asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + case None => throw UnsafeJson(JsonError.Message(s"Invalid enumeration value $typeName") :: trace) + } + case _ => throw UnsafeJson(JsonError.Message("Not a string") :: trace) + } + } + } + } else if (discrim.isEmpty) { // We're not allowing extra fields in this encoding new JsonDecoder[A] { val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_)) @@ -506,16 +534,18 @@ object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => } } +private lazy val caseObjectEncoder = new JsonEncoder[Any] { + def unsafeEncode(a: Any, indent: Option[Int], out: Write): Unit = + out.write("{}") + + override final def toJsonAST(a: Any): Either[String, Json] = + Right(Json.Obj(Chunk.empty)) +} + object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => def join[A](ctx: CaseClass[Typeclass, A]): JsonEncoder[A] = if (ctx.params.isEmpty) { - new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = - out.write("{}") - - override final def toJsonAST(a: A): Either[String, Json] = - Right(Json.Obj(Chunk.empty)) - } + caseObjectEncoder.narrow[A] } else { new JsonEncoder[A] { val (transformNames, nameTransform): (Boolean, String => String) = @@ -612,15 +642,49 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => } def split[A](ctx: SealedTrait[JsonEncoder, A]): JsonEncoder[A] = { + val isEnumeration = + (ctx.isEnum && ctx.subtypes.forall(_.typeclass == caseObjectEncoder)) || ( + !ctx.isEnum && ctx.subtypes.forall(_.isObject) + ) + val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(IdentityFormat) + val discrim = ctx .annotations .collectFirst { case jsonDiscriminator(n) => n } - if (discrim.isEmpty) { + if (isEnumeration) { + new JsonEncoder[A] { + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + val typeName = ctx.choose(a) { sub => + sub + .annotations + .collectFirst { + case jsonHint(name) => name + }.getOrElse(sub.typeInfo.short) + } + + JsonEncoder.string.unsafeEncode(typeName, indent, out) + } + + override final def toJsonAST(a: A): Either[String, Json] = { + ctx.choose(a) { sub => + Right( + Json.Str( + sub + .annotations + .collectFirst { + case jsonHint(name) => name + }.getOrElse(sub.typeInfo.short) + ) + ) + } + } + } + } else if (discrim.isEmpty) { new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { ctx.choose(a) { sub => diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala index a1520753c..b7b6379f0 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala @@ -9,25 +9,42 @@ object DerivedDecoderSpec extends ZIOSpecDefault { val spec = suite("DerivedDecoderSpec")( test("Derives for a product type") { - assertZIO(typeCheck { - """ - case class Foo(bar: String) derives JsonDecoder + case class Foo(bar: String) derives JsonDecoder - "{\"bar\": \"hello\"}".fromJson[Foo] - """ - })(isRight(anything)) + val result = "{\"bar\": \"hello\"}".fromJson[Foo] + + assertTrue(result == Right(Foo("hello"))) + }, + test("Derives for a sum enum Enumeration type") { + enum Foo derives JsonDecoder: + case Bar + case Baz + case Qux + + val result = "\"Qux\"".fromJson[Foo] + + assertTrue(result == Right(Foo.Qux)) }, - test("Derives for a sum type") { - assertZIO(typeCheck { - """ - enum Foo derives JsonDecoder: - case Bar - case Baz(baz: String) - case Qux(foo: Foo) - - "{\"Qux\":{\"foo\":{\"Bar\":{}}}}".fromJson[Foo] - """ - })(isRight(anything)) + test("Derives for a sum sealed trait Enumeration type") { + sealed trait Foo derives JsonDecoder + object Foo: + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + val result = "\"Qux\"".fromJson[Foo] + + assertTrue(result == Right(Foo.Qux)) + }, + test("Derives for a sum ADT type") { + enum Foo derives JsonDecoder: + case Bar + case Baz(baz: String) + case Qux(foo: Foo) + + val result = "{\"Qux\":{\"foo\":{\"Bar\":{}}}}".fromJson[Foo] + + assertTrue(result == Right(Foo.Qux(Foo.Bar))) }, test("Derives and decodes for a union of string-based literals") { case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonDecoder diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala index 9b7fc862d..2eb329c40 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala @@ -8,25 +8,42 @@ import zio.test._ object DerivedEncoderSpec extends ZIOSpecDefault { val spec = suite("DerivedEncoderSpec")( test("Derives for a product type") { - assertZIO(typeCheck { - """ - case class Foo(bar: String) derives JsonEncoder + case class Foo(bar: String) derives JsonEncoder - Foo("bar").toJson - """ - })(isRight(anything)) + val json = Foo("bar").toJson + + assertTrue(json == """{"bar":"bar"}""") }, - test("Derives for a sum type") { - assertZIO(typeCheck { - """ - enum Foo derives JsonEncoder: - case Bar - case Baz(baz: String) - case Qux(foo: Foo) - - (Foo.Qux(Foo.Bar): Foo).toJson - """ - })(isRight(anything)) + test("Derives for a sum enum Enumeration type") { + enum Foo derives JsonEncoder: + case Bar + case Baz + case Qux + + val json = (Foo.Qux: Foo).toJson + + assertTrue(json == """"Qux"""") + }, + test("Derives for a sum sealed trait Enumeration type") { + sealed trait Foo derives JsonEncoder + object Foo: + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + val json = (Foo.Qux: Foo).toJson + + assertTrue(json == """"Qux"""") + }, + test("Derives for a sum ADT type") { + enum Foo derives JsonEncoder: + case Bar + case Baz(baz: String) + case Qux(foo: Foo) + + val json = (Foo.Qux(Foo.Bar): Foo).toJson + + assertTrue(json == """{"Qux":{"foo":{"Bar":{}}}}""") }, test("Derives and encodes for a union of string-based literals") { case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonEncoder From 49304395c275299c8506989074b707960c7352d0 Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Mon, 17 Jun 2024 07:16:22 +0200 Subject: [PATCH 033/311] refactor: scala 3 const enum: simplify and cleanup (#1126) --- .../zio/json/JsonDecoderVersionSpecific.scala | 8 ------- .../zio/json/JsonEncoderVersionSpecific.scala | 3 --- .../scala-3/zio/json/union_derivation.scala | 22 ++++--------------- 3 files changed, 4 insertions(+), 29 deletions(-) diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala index 64f51eaee..ad020b005 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala @@ -17,12 +17,4 @@ trait DecoderLowPriorityVersionSpecific { case _ => Left("expected one of: " + values.toList.mkString(", ")) } ) - - inline given constStringToEnum[T <: String](using IsConst[T] =:= true): JsonDecoder[T] = - JsonDecoder.string.mapOrFail( - { - case raw if raw == constValue[T] => Right(constValue[T]) - case _ => Left("expected one of: " + constValue[T]) - } - ) } diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala index 97d623113..17f501c40 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala @@ -10,7 +10,4 @@ private[json] trait EncoderLowPriorityVersionSpecific { inline given unionOfStringEnumeration[T](using IsUnionOf[String, T]): JsonEncoder[T] = JsonEncoder.string.asInstanceOf[JsonEncoder[T]] - - inline given constStringToEnum[T <: String](using IsConst[T] =:= true): JsonEncoder[T] = - JsonEncoder.string.narrow[T] } diff --git a/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala index 0195b89c0..053092960 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala @@ -4,22 +4,6 @@ import scala.compiletime.* import scala.deriving.* import scala.quoted.* -@scala.annotation.implicitNotFound("${A} is not a union type") -sealed trait IsUnion[A] - -object IsUnion: - - private val singleton: IsUnion[Any] = new IsUnion[Any] {} - - transparent inline given derived[A]: IsUnion[A] = ${ deriveImpl[A] } - - private def deriveImpl[A](using quotes: Quotes, t: Type[A]): Expr[IsUnion[A]] = - import quotes.reflect.* - val tpe: TypeRepr = TypeRepr.of[A] - tpe.dealias match - case o: OrType => ('{ IsUnion.singleton.asInstanceOf[IsUnion[A]] }).asExprOf[IsUnion[A]] - case other => report.errorAndAbort(s"${tpe.show} is not a Union") - @scala.annotation.implicitNotFound("${A} is not a union of ${T}") sealed trait IsUnionOf[T, A] @@ -44,10 +28,12 @@ object IsUnionOf: else report.errorAndAbort(s"${o.show} is not a subtype of ${bound.show}") tpe.dealias match - case o: OrType => + case o: OrType => validateTypes(o) ('{ IsUnionOf.singleton.asInstanceOf[IsUnionOf[T, A]] }).asExprOf[IsUnionOf[T, A]] - case other => report.errorAndAbort(s"${tpe.show} is not a Union") + case o => + if o <:< bound then ('{ IsUnionOf.singleton.asInstanceOf[IsUnionOf[T, A]] }).asExprOf[IsUnionOf[T, A]] + else report.errorAndAbort(s"${tpe.show} is not a Union") object UnionDerivation: transparent inline def constValueUnionTuple[T, A](using IsUnionOf[T, A]): Tuple = ${ constValueUnionTupleImpl[T, A] } From 6d2abd46385af7002b193d52d19f49dbc42e0d01 Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:27:34 +0200 Subject: [PATCH 034/311] fix: make union_derivation private (#1133) --- .../shared/src/main/scala-3/zio/json/union_derivation.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala index 053092960..c260f472c 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala @@ -5,9 +5,9 @@ import scala.deriving.* import scala.quoted.* @scala.annotation.implicitNotFound("${A} is not a union of ${T}") -sealed trait IsUnionOf[T, A] +private[json] sealed trait IsUnionOf[T, A] -object IsUnionOf: +private[json] object IsUnionOf: private val singleton: IsUnionOf[Any, Any] = new IsUnionOf[Any, Any] {} @@ -35,7 +35,7 @@ object IsUnionOf: if o <:< bound then ('{ IsUnionOf.singleton.asInstanceOf[IsUnionOf[T, A]] }).asExprOf[IsUnionOf[T, A]] else report.errorAndAbort(s"${tpe.show} is not a Union") -object UnionDerivation: +private[json] object UnionDerivation: transparent inline def constValueUnionTuple[T, A](using IsUnionOf[T, A]): Tuple = ${ constValueUnionTupleImpl[T, A] } private def constValueUnionTupleImpl[T: Type, A: Type](using Quotes): Expr[Tuple] = From 154f6b1242040270caac3834cbf47ad1fb0efc69 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:26:58 +0200 Subject: [PATCH 035/311] Update dependencies (#1142) * Update dependencies * Update workflow * Mark macro test flaky * Downgrade Scala native to fix tests * Upgrade Scala native * Disable native multi threading * Disable native multi threading --- .github/workflows/ci.yml | 6 ++-- build.sbt | 34 ++++++++++--------- project/BuildHelper.scala | 11 +++--- project/plugins.sbt | 7 ++-- .../src/test/scala/zio/json/DeriveSpec.scala | 2 +- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bfc67eff..ee02a60b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v4.2.1 with: distribution: temurin - java-version: 11 + java-version: 21 check-latest: true - name: Cache scala dependencies uses: coursier/cache-action@v6 @@ -37,7 +37,7 @@ jobs: strategy: fail-fast: false matrix: - java: ['11', '17'] + java: ['17', '21'] scala: ['2.13.13'] steps: - name: Checkout current branch @@ -78,7 +78,7 @@ jobs: strategy: fail-fast: false matrix: - java: ['11', '17'] + java: ['17', '21'] scala: ['2.12.19', '2.13.13', '3.3.3'] platform: ['JVM', 'JS', 'Native'] steps: diff --git a/build.sbt b/build.sbt index 08edd179e..d8afc9e9e 100644 --- a/build.sbt +++ b/build.sbt @@ -55,7 +55,7 @@ addCommandAlias( "zioJsonNative/test; zioJsonInteropScalaz7xNative/test" ) -val zioVersion = "2.0.21" +val zioVersion = "2.1.7" lazy val zioJsonRoot = project .in(file(".")) @@ -82,7 +82,7 @@ lazy val zioJsonRoot = project zioJsonGolden ) -val circeVersion = "0.14.3" +val circeVersion = "0.14.9" lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("zio-json")) @@ -105,7 +105,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) libraryDependencies ++= Seq( "dev.zio" %%% "zio" % zioVersion, "dev.zio" %%% "zio-streams" % zioVersion, - "org.scala-lang.modules" %%% "scala-collection-compat" % "2.9.0", + "org.scala-lang.modules" %%% "scala-collection-compat" % "2.12.0", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", "io.circe" %%% "circe-core" % circeVersion % "test", @@ -117,16 +117,16 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Vector( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.0" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.7" ) case _ => Vector( "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, - "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.8", - "io.circe" %%% "circe-generic-extras" % circeVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.23.3" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.23.3" % "test" + "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.10", + "io.circe" %%% "circe-generic-extras" % "0.14.4" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.7" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.7" % "test" ) } }, @@ -218,8 +218,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) .jsSettings( libraryDependencies ++= Seq( - "io.github.cquiroz" %%% "scala-java-time" % "2.5.0", - "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.6.0" + "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, + "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion ) ) .jvmSettings( @@ -253,8 +253,9 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .nativeSettings(Test / fork := false) .nativeSettings( libraryDependencies ++= Seq( - "io.github.cquiroz" %%% "scala-java-time" % "2.5.0" - ) + "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion + ), + nativeConfig ~= { _.withMultithreading(false) } ) .enablePlugins(BuildInfoPlugin) @@ -313,7 +314,8 @@ lazy val zioJsonMacros = crossProject(JSPlatform, JVMPlatform, NativePlatform) "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test" ), - testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), + nativeConfig ~= { _.withMultithreading(false) } ) .nativeSettings(Test / fork := false) @@ -333,7 +335,7 @@ lazy val zioJsonInteropHttp4s = project "org.http4s" %% "http4s-dsl" % "0.23.26", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.4.9", - "dev.zio" %% "zio-interop-cats" % "23.0.03" % "test", + "dev.zio" %% "zio-interop-cats" % "23.1.0.3" % "test", "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" ), @@ -349,7 +351,7 @@ lazy val zioJsonInteropRefined = crossProject(JSPlatform, JVMPlatform, NativePla .settings(buildInfoSettings("zio.json.interop.refined")) .settings( libraryDependencies ++= Seq( - "eu.timepit" %%% "refined" % "0.10.2", + "eu.timepit" %%% "refined" % "0.11.2", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test" ), @@ -365,7 +367,7 @@ lazy val zioJsonInteropScalaz7x = crossProject(JSPlatform, JVMPlatform, NativePl .settings( crossScalaVersions -= ScalaDotty, libraryDependencies ++= Seq( - "org.scalaz" %%% "scalaz-core" % "7.3.7", + "org.scalaz" %%% "scalaz-core" % "7.3.8", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test" ), diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 9fb861450..abc9e8281 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -264,10 +264,13 @@ object BuildHelper { } ) - def jsSettings = Seq( - libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.2.2", - libraryDependencies += "io.github.cquiroz" %%% "scala-java-time-tzdb" % "2.2.2" - ) + val scalaJavaTimeVersion = "2.6.0" + + def jsSettings = + Seq( + libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, + libraryDependencies += "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion + ) def nativeSettings = Seq( Test / skip := true, diff --git a/project/plugins.sbt b/project/plugins.sbt index a68356100..e62b4e072 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,13 +3,14 @@ addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.11") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.1") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") -addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.4") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.28") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.27") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.7" diff --git a/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala b/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala index c88f87a12..fe1d95847 100644 --- a/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala +++ b/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala @@ -54,7 +54,7 @@ object DeriveSpec extends ZIOSpecDefault { assert("""{"hint":"Child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && assert("""{"child1":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) } - ) + ) @@ TestAspect.flaky // flaky only for Scala Native ) object exampleproducts { From 0a555ac416f28daa67e2ee5b0eecb68b2471c553 Mon Sep 17 00:00:00 2001 From: "Patrick D. Conti" Date: Thu, 15 Aug 2024 06:27:33 +0200 Subject: [PATCH 036/311] fix(JsonCodec): codec derivation on nested types (#1139) --- .../scala-2.12/zio/json/JsonCodecVersionSpecific.scala | 3 --- .../scala-2.13/zio/json/JsonCodecVersionSpecific.scala | 7 ------- .../src/main/scala-2.x/JsonCodecVersionSpecific.scala | 3 +++ .../main/scala-3/zio/json/JsonCodecVersionSpecific.scala | 2 -- zio-json/shared/src/main/scala/zio/json/JsonCodec.scala | 3 +++ 5 files changed, 6 insertions(+), 12 deletions(-) delete mode 100644 zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala delete mode 100644 zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala create mode 100644 zio-json/shared/src/main/scala-2.x/JsonCodecVersionSpecific.scala diff --git a/zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala deleted file mode 100644 index e57d1dc36..000000000 --- a/zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala +++ /dev/null @@ -1,3 +0,0 @@ -package zio.json - -private[json] trait JsonCodecVersionSpecific diff --git a/zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala deleted file mode 100644 index f3292ed19..000000000 --- a/zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala +++ /dev/null @@ -1,7 +0,0 @@ -package zio.json - -private[json] trait JsonCodecVersionSpecific { - - implicit def fromEncoderDecoder[A](implicit encoder: JsonEncoder[A], decoder: JsonDecoder[A]): JsonCodec[A] = - JsonCodec(encoder, decoder) -} diff --git a/zio-json/shared/src/main/scala-2.x/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-2.x/JsonCodecVersionSpecific.scala new file mode 100644 index 000000000..cfe9b163e --- /dev/null +++ b/zio-json/shared/src/main/scala-2.x/JsonCodecVersionSpecific.scala @@ -0,0 +1,3 @@ +package zio.json + +trait JsonCodecVersionSpecific diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala index 9b0319709..4d309c805 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala @@ -3,6 +3,4 @@ package zio.json private[json] trait JsonCodecVersionSpecific { inline def derived[A: deriving.Mirror.Of]: JsonCodec[A] = DeriveJsonCodec.gen[A] - given fromEncoderDecoder[A](using encoder: JsonEncoder[A], decoder: JsonDecoder[A]): JsonCodec[A] = - JsonCodec(encoder, decoder) } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala index fdad66d18..7034775b9 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala @@ -87,6 +87,9 @@ final case class JsonCodec[A](encoder: JsonEncoder[A], decoder: JsonDecoder[A]) object JsonCodec extends GeneratedTupleCodecs with CodecLowPriority0 with JsonCodecVersionSpecific { def apply[A](implicit jsonCodec: JsonCodec[A]): JsonCodec[A] = jsonCodec + implicit def fromEncoderDecoder[A](encoder: JsonEncoder[A], decoder: JsonDecoder[A]): JsonCodec[A] = + JsonCodec(encoder, decoder) + private def orElseEither[A, B](A: JsonCodec[A], B: JsonCodec[B]): JsonCodec[Either[A, B]] = JsonCodec( JsonEncoder.orElseEither[A, B](A.encoder, B.encoder), From 6395b8ecaef16d3a969d6274877dc30f1b618e2a Mon Sep 17 00:00:00 2001 From: Petter Date: Tue, 20 Aug 2024 08:19:53 +0200 Subject: [PATCH 037/311] JsonFieldEncoder for uuid (#1144) * JsonFieldEncoder for uuid * unused * formatting --------- Co-authored-by: Petter Kamfjord --- .../scala/zio/json/JsonFieldDecoder.scala | 20 ++++++++++ .../scala/zio/json/JsonFieldEncoder.scala | 4 ++ .../src/test/scala/zio/json/DecoderSpec.scala | 39 +++++++++++++++++++ .../src/test/scala/zio/json/EncoderSpec.scala | 5 +++ 4 files changed, 68 insertions(+) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala index 68fc213cf..d25f6adf7 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala @@ -15,6 +15,8 @@ */ package zio.json +import zio.json.uuid.UUIDParser + /** When decoding a JSON Object, we only allow the keys that implement this interface. */ trait JsonFieldDecoder[+A] { self => @@ -64,4 +66,22 @@ object JsonFieldDecoder { case n: NumberFormatException => Left(s"Invalid Long: '$str': $n") } } + + implicit val uuid: JsonFieldDecoder[java.util.UUID] = mapStringOrFail { str => + try { + Right(UUIDParser.unsafeParse(str)) + } catch { + case iae: IllegalArgumentException => Left(s"Invalid UUID: ${iae.getMessage}") + } + } + + // use this instead of `string.mapOrFail` in supertypes (to prevent class initialization error at runtime) + private[json] def mapStringOrFail[A](f: String => Either[String, A]): JsonFieldDecoder[A] = + new JsonFieldDecoder[A] { + def unsafeDecodeField(trace: List[JsonError], in: String): A = + f(string.unsafeDecodeField(trace, in)) match { + case Left(err) => throw JsonDecoder.UnsafeJson(JsonError.Message(err) :: trace) + case Right(value) => value + } + } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala index 706bac5ae..d2b1e156c 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala @@ -38,4 +38,8 @@ object JsonFieldEncoder { implicit val long: JsonFieldEncoder[Long] = JsonFieldEncoder[String].contramap(_.toString) + + implicit val uuid: JsonFieldEncoder[java.util.UUID] = + JsonFieldEncoder[String].contramap(_.toString) + } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index acb23e86f..73353af82 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -241,6 +241,45 @@ object DecoderSpec extends ZIOSpecDefault { val jsonStr = JsonEncoder[Map[String, String]].encodeJson(expected, None) assert(jsonStr.fromJson[Map[String, String]])(isRight(equalTo(expected))) }, + test("Map with UUID keys") { + def expectedMap(str: String): Map[UUID, String] = Map(UUID.fromString(str) -> "value") + + val ok1 = """{"64d7c38d-2afd-4514-9832-4e70afe4b0f8": "value"}""" + val ok2 = """{"0000000064D7C38D-FD-14-32-70AFE4B0f8": "value"}""" + val ok3 = """{"0-0-0-0-0": "value"}""" + val bad1 = """{"": "value"}""" + val bad2 = """{"64d7c38d-2afd-4514-9832-4e70afe4b0f80": "value"}""" + val bad3 = """{"64d7c38d-2afd-4514-983-4e70afe4b0f80": "value"}""" + val bad4 = """{"64d7c38d-2afd--9832-4e70afe4b0f8": "value"}""" + val bad5 = """{"64d7c38d-2afd-XXXX-9832-4e70afe4b0f8": "value"}""" + val bad6 = """{"64d7c38d-2afd-X-9832-4e70afe4b0f8": "value"}""" + val bad7 = """{"0-0-0-0-00000000000000000": "value"}""" + + assert(ok1.fromJson[Map[UUID, String]])( + isRight(equalTo(expectedMap("64d7c38d-2afd-4514-9832-4e70afe4b0f8"))) + ) && + assert(ok2.fromJson[Map[UUID, String]])( + isRight(equalTo(expectedMap("64D7C38D-00FD-0014-0032-0070AfE4B0f8"))) + ) && + assert(ok3.fromJson[Map[UUID, String]])( + isRight(equalTo(expectedMap("00000000-0000-0000-0000-000000000000"))) + ) && + assert(bad1.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: "))) && + assert(bad2.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: UUID string too large"))) && + assert(bad3.fromJson[Map[UUID, String]])( + isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80")) + ) && + assert(bad4.fromJson[Map[UUID, String]])( + isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8")) + ) && + assert(bad5.fromJson[Map[UUID, String]])( + isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8")) + ) && + assert(bad6.fromJson[Map[UUID, String]])( + isLeft(containsString("Invalid UUID: 64d7c38d-2afd-X-9832-4e70afe4b0f8")) + ) && + assert(bad7.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 0-0-0-0-00000000000000000"))) + }, test("zio.Chunk") { val jsonStr = """["5XL","2XL","XL"]""" val expected = Chunk("5XL", "2XL", "XL") diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index f8b895ec1..59f484d48 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -261,6 +261,11 @@ object EncoderSpec extends ZIOSpecDefault { test("Map, custom keys") { assert(Map(1 -> "a").toJson)(equalTo("""{"1":"a"}""")) }, + test("Map, UUID keys") { + assert(Map(UUID.fromString("e142f1aa-6e9e-4352-adfe-7e6eb9814ccd") -> "abcd").toJson)( + equalTo("""{"e142f1aa-6e9e-4352-adfe-7e6eb9814ccd":"abcd"}""") + ) + }, test("java.util.UUID") { assert(UUID.fromString("e142f1aa-6e9e-4352-adfe-7e6eb9814ccd").toJson)( equalTo(""""e142f1aa-6e9e-4352-adfe-7e6eb9814ccd"""") From 2174436c4bed3470b37751a2ac8f7f2b7acee53e Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Wed, 28 Aug 2024 14:45:32 +1000 Subject: [PATCH 038/311] Fix golden tests generation for `Gen` instances using `filter` (#1152) * Add failing test * Fix golden tests generation for `Gen` instances using `filter` * Fix tests --- .../main/scala/zio/json/golden/package.scala | 19 +- .../filteredgentype/FilteredGenType.json | 304 ++++++++++++++++++ .../scala/zio/json/golden/GoldenSpec.scala | 29 +- 3 files changed, 339 insertions(+), 13 deletions(-) create mode 100644 zio-json-golden/src/test/resources/golden/filteredgentype/FilteredGenType.json diff --git a/zio-json-golden/src/main/scala/zio/json/golden/package.scala b/zio-json-golden/src/main/scala/zio/json/golden/package.scala index 8a8cbc4c3..e36fcfc7c 100644 --- a/zio-json-golden/src/main/scala/zio/json/golden/package.scala +++ b/zio-json-golden/src/main/scala/zio/json/golden/package.scala @@ -97,21 +97,20 @@ package object golden { } yield TestResult(assertion) } + /** + * Implementation inspired by zio-test [[zio.test#check]] + */ private def generateSample[A: JsonEncoder]( gen: Gen[Sized, A], sampleSize: Int )(implicit trace: Trace): ZIO[Sized, Exception, GoldenSample] = - Gen - .listOfN(sampleSize)(gen) - .sample + gen.sample.forever .map(_.value) - .map { elements => - val jsonElements = elements.map(_.toJsonAST).collect { case Right(a) => a } - val jsonArray = new Json.Arr(Chunk.fromIterable(jsonElements)) - GoldenSample(jsonArray) - } - .runHead - .someOrFailException + .map(_.toJsonAST) + .collectRight + .take(sampleSize.toLong) + .runCollect + .map(jsonElements => GoldenSample(new Json.Arr(jsonElements))) private def getName[A](implicit tag: Tag[A]): String = tag.tag.shortName diff --git a/zio-json-golden/src/test/resources/golden/filteredgentype/FilteredGenType.json b/zio-json-golden/src/test/resources/golden/filteredgentype/FilteredGenType.json new file mode 100644 index 000000000..cbff71868 --- /dev/null +++ b/zio-json-golden/src/test/resources/golden/filteredgentype/FilteredGenType.json @@ -0,0 +1,304 @@ +{ + "samples" : [ + { + "a" : -6.807778209396064042484608633080144E+37 + }, + { + "a" : 1.595098341748072691735235811828587E+38 + }, + { + "a" : 1.459950313422007732470858735428470E+38 + }, + { + "a" : 3.966890979309949040353919413508109E+37 + }, + { + "a" : 1.233609631287946015973115353212111E+38 + }, + { + "a" : 1.473182672254656075288635468340979E+38 + }, + { + "a" : -4.370307886719313347409020693679809E+36 + }, + { + "a" : 4.037946164963809325160258151515959E+37 + }, + { + "a" : 9.654807468219847939517983223821157E+36 + }, + { + "a" : 9.016039177425153953741714882797374E+37 + }, + { + "a" : 7.146160681625873270982431039786890E+37 + }, + { + "a" : 5.715520023941784610636805140974297E+37 + }, + { + "a" : -4.963886209068154507584885110622067E+37 + }, + { + "a" : 1.429642469253121453233931676540811E+38 + }, + { + "a" : 1.347358173024022215551559165152039E+38 + }, + { + "a" : -1.443197818206812225540268705601235E+38 + }, + { + "a" : 1.652974714985098210216497615438388E+38 + }, + { + "a" : 6.854881439246725349714811941975828E+37 + }, + { + "a" : -1.440269453811987772460377566176060E+38 + }, + { + "a" : 7.349647466961309857751700891214103E+37 + }, + { + "a" : 1.442405212658813209066752593353578E+38 + }, + { + "a" : 3.965771366281331473297193084703031E+37 + }, + { + "a" : 6.269323508703350877363225990119655E+37 + }, + { + "a" : -1.758914181069706042842822199462931E+37 + }, + { + "a" : 1.223935114914264676851645426397511E+38 + }, + { + "a" : -7.384928634462281384291440360868614E+37 + }, + { + "a" : 1.693228930919227030554211724500322E+38 + }, + { + "a" : 4.424974721701017780262797468032916E+37 + }, + { + "a" : -1.303664216711215462163176389172558E+38 + }, + { + "a" : 1.404704317795160461166424204194676E+37 + }, + { + "a" : -1.329057724454004799664476545910425E+38 + }, + { + "a" : -1.048356224872675570271942686628903E+38 + }, + { + "a" : -6.290438873817500525528580887751261E+37 + }, + { + "a" : 1.084524705805485052810712065828027E+38 + }, + { + "a" : -7.550243566246841354983346269026909E+37 + }, + { + "a" : 9.340651914763911687206557126706644E+37 + }, + { + "a" : -2.480333693026775008020223168834843E+37 + }, + { + "a" : -1.094510132373439403344708257152704E+38 + }, + { + "a" : -6.786566488714105384835344334324745E+37 + }, + { + "a" : 1.584399487952599918984676750023353E+37 + }, + { + "a" : 5.128682031007228158346770228279368E+37 + }, + { + "a" : 9.348340016921799959253942302421099E+37 + }, + { + "a" : 7.030935368599146218716189376901462E+37 + }, + { + "a" : 1.535594588509527431040574927018135E+38 + }, + { + "a" : -4.934741325633573501964215791643639E+37 + }, + { + "a" : 9.682312857134232428245162228518594E+37 + }, + { + "a" : -2.614986691569376906824897939793061E+37 + }, + { + "a" : -1.014394404488501605028181495043210E+38 + }, + { + "a" : 5.488115260327267108034268022746715E+37 + }, + { + "a" : 1.126087665079675493640367522377595E+38 + }, + { + "a" : -1.039540496026219616948722255179099E+38 + }, + { + "a" : -9.140747409014218126917907727764322E+37 + }, + { + "a" : -4.824125603843691307227014286009879E+37 + }, + { + "a" : -8.145094645621006028171195553277435E+37 + }, + { + "a" : -5.626674097898124181091040103617824E+37 + }, + { + "a" : 9.103992673978668331401073051758253E+37 + }, + { + "a" : -6.434701867932310680029743191675950E+37 + }, + { + "a" : 1.544388056858114009506299836014695E+37 + }, + { + "a" : -1.265122848783281340816555691730657E+38 + }, + { + "a" : -1.038801113752491268000223564191006E+38 + }, + { + "a" : 1.005113855834286665723509372286190E+38 + }, + { + "a" : -1.532637041640666300354819845360303E+38 + }, + { + "a" : 1.460152591686346812838507724273444E+37 + }, + { + "a" : -1.807215480356152323767541674888201E+37 + }, + { + "a" : 1.130164367464931673447884683370859E+38 + }, + { + "a" : -2.206605703514989373349781667972389E+37 + }, + { + "a" : -7.335310305765769880508310438610933E+37 + }, + { + "a" : -9.032060789190847023210378392783773E+37 + }, + { + "a" : -1.434866879379575685137940146522389E+38 + }, + { + "a" : -8.537143289319745801149293653774310E+37 + }, + { + "a" : -7.189751213495862580473152406825669E+37 + }, + { + "a" : -1.116003336173084398830455517338484E+38 + }, + { + "a" : 4.350602216631428056662100784595331E+37 + }, + { + "a" : -4.200580225109217264680432341389498E+37 + }, + { + "a" : 1.519464406997130639901449647551254E+38 + }, + { + "a" : 1.605847304967111250934189072078944E+38 + }, + { + "a" : -1.537207767079682775719785647758733E+38 + }, + { + "a" : -1.279659726955884472452164873447049E+38 + }, + { + "a" : 6.508277213006463765700394170604852E+37 + }, + { + "a" : 1.283817488073224657514525683762202E+38 + }, + { + "a" : 1.339367218367240868953919097436571E+38 + }, + { + "a" : -1.682265900214633524445901815287998E+37 + }, + { + "a" : 1.206863685039632397166752202964046E+38 + }, + { + "a" : -1.084738675922968416633463809761249E+38 + }, + { + "a" : 5.657051930790282199429825000570743E+37 + }, + { + "a" : -8.295559405291592878379291027574690E+36 + }, + { + "a" : 1.759196447669202351875403477421470E+37 + }, + { + "a" : 1.469253810134078897550508376470893E+38 + }, + { + "a" : 4.042754662729660664430342317410895E+37 + }, + { + "a" : -2.203950127770040611464116843938096E+37 + }, + { + "a" : 1.510320473631250916474309414975180E+38 + }, + { + "a" : -7.763024290460987157407648151549977E+37 + }, + { + "a" : 2.621985659558485942857886646450931E+37 + }, + { + "a" : -7.736585181363804829301628996321853E+37 + }, + { + "a" : -6.009164305461593191654380749583999E+37 + }, + { + "a" : -9.519320687479863998450437188934807E+37 + }, + { + "a" : -3.199912116712397916450907359672628E+37 + }, + { + "a" : 1.050603568467569482115229536968020E+38 + }, + { + "a" : -1.259168689102438672889911917677648E+38 + }, + { + "a" : -1.203951555022847927293048112388223E+38 + } + ] +} \ No newline at end of file diff --git a/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala b/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala index 01b539033..89eb98d92 100644 --- a/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala +++ b/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala @@ -1,10 +1,10 @@ package zio.json.golden +import zio._ import zio.json._ -import zio.json.golden._ +import zio.test.TestAspect.exceptScala212 import zio.test._ import zio.test.magnolia.DeriveGen -import zio._ object GoldenSpec extends ZIOSpecDefault { @@ -29,6 +29,25 @@ object GoldenSpec extends ZIOSpecDefault { implicit val jsonCodec: JsonCodec[RecordType] = DeriveJsonCodec.gen } + final case class FilteredGenType(a: java.math.BigDecimal) + object FilteredGenType { + implicit val jsonCodec: JsonCodec[FilteredGenType] = DeriveJsonCodec.gen + + val anyFilteredGenType: Gen[Any, FilteredGenType] = { + + /** + * Copied from zio-json/shared/src/test/scala/zio/json/Gens.scala + */ + val genBigDecimal: Gen[Any, java.math.BigDecimal] = + Gen + .bigDecimal((BigDecimal(2).pow(128) - 1) * -1, BigDecimal(2).pow(128) - 1) + .map(_.bigDecimal) + .filter(_.toBigInteger.bitLength < 128) + + genBigDecimal.map(FilteredGenType.apply) + } + } + def spec: Spec[TestEnvironment with Scope, Any] = suite("GoldenSpec")( goldenTest(DeriveGen[Int]), goldenTest(DeriveGen[SumType]), @@ -41,7 +60,11 @@ object GoldenSpec extends ZIOSpecDefault { }, { implicit val config: GoldenConfiguration = GoldenConfiguration.default.copy(relativePath = "recordtype") goldenTest(DeriveGen[RecordType]) - } + }, { + implicit val config: GoldenConfiguration = + GoldenConfiguration.default.copy(relativePath = "filteredgentype", sampleSize = 100) + goldenTest(FilteredGenType.anyFilteredGenType) + } @@ exceptScala212 // Quick & Dirty fix. Scala 2.12 generates BigDecimal differently making the test fail for no good reason. ) } From 30ef40c4d225b0b83253e4642157ad3c60319a5e Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 1 Sep 2024 14:24:56 +0200 Subject: [PATCH 039/311] Update jsoniter-scala-core, ... to 2.30.9 (#1153) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index d8afc9e9e..2db11eecc 100644 --- a/build.sbt +++ b/build.sbt @@ -125,8 +125,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.10", "io.circe" %%% "circe-generic-extras" % "0.14.4" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.7" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.7" % "test" + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.9" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.9" % "test" ) } }, From d699c04f4281e5fde9978bfd5f3270128b03d5f9 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 1 Sep 2024 14:25:12 +0200 Subject: [PATCH 040/311] Update snakeyaml to 2.3 (#1155) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 2db11eecc..395dc7826 100644 --- a/build.sbt +++ b/build.sbt @@ -290,7 +290,7 @@ lazy val zioJsonYaml = project .settings(buildInfoSettings("zio.json.yaml")) .settings( libraryDependencies ++= Seq( - "org.yaml" % "snakeyaml" % "2.2", + "org.yaml" % "snakeyaml" % "2.3", "dev.zio" %% "zio" % zioVersion, "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" From f50273b6d3bef28bbd22a00e4bf0e8be8b991294 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 1 Sep 2024 14:25:44 +0200 Subject: [PATCH 041/311] Update sbt-ci-release to 1.6.1 (#1147) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index e62b4e072..d0070f9b5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.11") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.6.1") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.1") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") From e88fcd9eb24dcf9f56ac860837b179f5c98bd72e Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 1 Sep 2024 14:25:58 +0200 Subject: [PATCH 042/311] Update sbt-mima-plugin to 1.1.4 (#1146) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index d0070f9b5..937dbb98c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.6.1") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") -addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.1") +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") From e8b1b79d2ae2be2c673e80434984e19032ff2658 Mon Sep 17 00:00:00 2001 From: Jisoo Park Date: Tue, 17 Sep 2024 18:56:16 +0900 Subject: [PATCH 043/311] Respect discriminators of object enums in Scala 3 (#1160) --- .../shared/src/main/scala-3/zio/json/macros.scala | 4 ++-- .../test/scala-3/zio/json/DerivedDecoderSpec.scala | 12 ++++++++++++ .../test/scala-3/zio/json/DerivedEncoderSpec.scala | 11 +++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 3392d476a..f5f04c4c6 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -409,7 +409,7 @@ object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n } - if (isEnumeration) { + if (isEnumeration && discrim.isEmpty) { new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val typeName = Lexer.string(trace, in).toString() @@ -656,7 +656,7 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => case jsonDiscriminator(n) => n } - if (isEnumeration) { + if (isEnumeration && discrim.isEmpty) { new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { val typeName = ctx.choose(a) { sub => diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala index b7b6379f0..b99dd7cd2 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala @@ -36,6 +36,18 @@ object DerivedDecoderSpec extends ZIOSpecDefault { assertTrue(result == Right(Foo.Qux)) }, + test("Derives for a sum sealed trait Enumeration type with discriminator") { + @jsonDiscriminator("$type") + sealed trait Foo derives JsonDecoder + object Foo: + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + val result = """{"$type":"Qux"}""".fromJson[Foo] + + assertTrue(result == Right(Foo.Qux)) + }, test("Derives for a sum ADT type") { enum Foo derives JsonDecoder: case Bar diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala index 2eb329c40..05e28dd6e 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala @@ -24,6 +24,17 @@ object DerivedEncoderSpec extends ZIOSpecDefault { assertTrue(json == """"Qux"""") }, + test("Derives for a sum enum Enumeration type with discriminator") { + @jsonDiscriminator("$type") + enum Foo derives JsonEncoder: + case Bar + case Baz + case Qux + + val json = (Foo.Qux: Foo).toJson + + assertTrue(json == """{"$type":"Qux"}""") + }, test("Derives for a sum sealed trait Enumeration type") { sealed trait Foo derives JsonEncoder object Foo: From 7066a54e17dcf2f09b8201b5739860961fc58f70 Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:11:59 +0200 Subject: [PATCH 044/311] feat: support ListMap (#1177) --- zio-json/shared/src/main/scala/zio/json/JsonCodec.scala | 4 ++++ zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala | 7 +++++++ zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala | 3 +++ zio-json/shared/src/test/scala/zio/json/CodecSpec.scala | 6 ++++++ zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala | 6 ++++++ zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala | 3 +++ 6 files changed, 29 insertions(+) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala index 7034775b9..32e3c53ec 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala @@ -157,6 +157,10 @@ private[json] trait CodecLowPriority1 extends CodecLowPriority2 { this: JsonCode implicit def sortedSet[A: Ordering: JsonEncoder: JsonDecoder]: JsonCodec[immutable.SortedSet[A]] = JsonCodec(JsonEncoder.sortedSet[A], JsonDecoder.sortedSet[A]) + + implicit def listMap[K: JsonFieldEncoder: JsonFieldDecoder, V: JsonEncoder: JsonDecoder] + : JsonCodec[immutable.ListMap[K, V]] = + JsonCodec(JsonEncoder.listMap[K, V], JsonDecoder.listMap[K, V]) } private[json] trait CodecLowPriority2 extends CodecLowPriority3 { this: JsonCodec.type => diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index ddf45c725..5be8a54ec 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -597,6 +597,13 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { def unsafeDecode(trace: List[JsonError], in: RetractReader): collection.SortedMap[K, V] = keyValueBuilder(trace, in, collection.SortedMap.newBuilder[K, V]) } + + implicit def listMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.ListMap[K, V]] = + new JsonDecoder[immutable.ListMap[K, V]] { + + def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.ListMap[K, V] = + keyValueBuilder(trace, in, immutable.ListMap.newBuilder[K, V]) + } } // We have a hierarchy of implicits for two reasons: diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 6b1dc40e6..7098a1b83 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -377,6 +377,9 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { implicit def sortedMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[collection.SortedMap[K, V]] = keyValueIterable[K, V, collection.SortedMap] + + implicit def listMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[immutable.ListMap[K, V]] = + keyValueIterable[K, V, immutable.ListMap] } private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { diff --git a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala index 57f70b132..1cd755c73 100644 --- a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala @@ -182,6 +182,12 @@ object CodecSpec extends ZIOSpecDefault { assert(jsonStr.fromJson[Map[String, Int]])(isRight(equalTo(expected))) }, + test("ListMap") { + val jsonStr = """{"5XL":3,"2XL":14,"XL":159}""" + val expected = collection.immutable.ListMap("5XL" -> 3, "2XL" -> 14, "XL" -> 159) + + assert(jsonStr.fromJson[collection.immutable.ListMap[String, Int]])(isRight(equalTo(expected))) + }, test("zio.Chunk") { val jsonStr = """["5XL","2XL","XL"]""" val expected = Chunk("5XL", "2XL", "XL") diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 73353af82..7f65c7523 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -494,6 +494,12 @@ object DecoderSpec extends ZIOSpecDefault { assert(json.as[SortedMap[String, Int]])(isRight(equalTo(expected))) }, + test("ListMap") { + val json = Json.Obj("5XL" -> Json.Num(3), "2XL" -> Json.Num(14), "XL" -> Json.Num(159)) + val expected = immutable.ListMap("5XL" -> 3, "2XL" -> 14, "XL" -> 159) + + assert(json.as[immutable.ListMap[String, Int]])(isRight(equalTo(expected))) + }, test("Map, custom keys") { val json = Json.Obj("1" -> Json.Str("a"), "2" -> Json.Str("b")) val expected = Map(1 -> "a", 2 -> "b") diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index 59f484d48..914099e64 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -256,6 +256,9 @@ object EncoderSpec extends ZIOSpecDefault { assert(Map("hello" -> "world").toJsonPretty)(equalTo("{\n \"hello\" : \"world\"\n}")) && assert(Map("hello" -> Some("world"), "goodbye" -> None).toJsonPretty)( equalTo("{\n \"hello\" : \"world\"\n}") + ) && + assert(immutable.ListMap("hello" -> "world", "goodbye" -> "world").toJson)( + equalTo("""{"hello":"world","goodbye":"world"}""") ) }, test("Map, custom keys") { From 717ce1a38a5e2bf42f7a2b12d240b10db115317b Mon Sep 17 00:00:00 2001 From: ologbonowiwi Date: Sun, 20 Oct 2024 14:12:41 -0300 Subject: [PATCH 045/311] feat: enable decoding for 256bit number (#1135) * test: ensure >256 bit does not work but 256 works * feat: update max bit to 256 * style: format file * test(CodecSpec): update big int size --- .../src/main/scala/zio/json/internal/lexer.scala | 2 +- .../src/test/scala/zio/json/CodecSpec.scala | 12 ++++++++---- .../src/test/scala/zio/json/DecoderSpec.scala | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index c4e66505c..39dc7fd2a 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -26,7 +26,7 @@ object Lexer { // TODO need a variant that doesn't skip whitespace, so that attack vectors // consisting of an infinite stream of space can exit early. - val NumberMaxBits: Int = 128 + val NumberMaxBits: Int = 256 // True if we got a string (implies a retraction), False for } def firstField(trace: List[JsonError], in: RetractReader): Boolean = diff --git a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala index 1cd755c73..468017ecc 100644 --- a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala @@ -7,6 +7,7 @@ import zio.test.Assertion._ import zio.test.TestAspect.jvmOnly import zio.test._ +import java.math.BigInteger import scala.collection.immutable object CodecSpec extends ZIOSpecDefault { @@ -37,10 +38,13 @@ object CodecSpec extends ZIOSpecDefault { }, test("primitives") { val exampleBDString = "234234.234" - // this big integer consumes more than 128 bits - assert("170141183460469231731687303715884105728".fromJson[java.math.BigInteger])( - isLeft(equalTo("(expected a 128 bit BigInteger)")) - ) && assert(exampleBDString.fromJson[BigDecimal])(isRight(equalTo(BigDecimal(exampleBDString)))) + // this big integer consumes more than 256 bits + assert( + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" + .fromJson[java.math.BigInteger] + )( + isLeft(equalTo("(expected a 256 bit BigInteger)")) + ) }, test("java.util.Currency") { val exampleValue = "\"USD\"" diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 7f65c7523..fd7088738 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -7,6 +7,7 @@ import zio.test.Assertion._ import zio.test.TestAspect.jvmOnly import zio.test._ +import java.math.BigInteger import java.time.{ Duration, OffsetDateTime, ZonedDateTime } import java.util.UUID import scala.collection.{ SortedMap, immutable, mutable } @@ -19,10 +20,18 @@ object DecoderSpec extends ZIOSpecDefault { test("BigDecimal") { assert("123".fromJson[BigDecimal])(isRight(equalTo(BigDecimal(123)))) }, - test("BigInteger too large") { - // this big integer consumes more than 128 bits + test("256 bit BigInteger") { assert("170141183460469231731687303715884105728".fromJson[java.math.BigInteger])( - isLeft(equalTo("(expected a 128 bit BigInteger)")) + isRight(equalTo(new BigInteger("170141183460469231731687303715884105728"))) + ) + }, + test("BigInteger too large") { + // this big integer consumes more than 256 bits + assert( + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" + .fromJson[java.math.BigInteger] + )( + isLeft(equalTo("(expected a 256 bit BigInteger)")) ) }, test("collections") { From 5eeec667df6a21b73ac7f22d9b98571680493078 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:12:50 +0200 Subject: [PATCH 046/311] Update snakeyaml-engine to 2.8 (#1162) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 937dbb98c..aa20271d8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -13,4 +13,4 @@ addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.28") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.27") -libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.7" +libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.8" From a190a1ae6f568f89728b68909fec9e6b42dddbbf Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:12:57 +0200 Subject: [PATCH 047/311] Update circe-core, circe-generic, ... to 0.14.10 (#1156) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 395dc7826..93b9f7c52 100644 --- a/build.sbt +++ b/build.sbt @@ -82,7 +82,7 @@ lazy val zioJsonRoot = project zioJsonGolden ) -val circeVersion = "0.14.9" +val circeVersion = "0.14.10" lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("zio-json")) From 46c4b4f9419f61d7efa0118fb3204d83f45dd4d0 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:13:40 +0200 Subject: [PATCH 048/311] Update sbt-scalajs, scalajs-compiler, ... to 1.17.0 (#1166) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index aa20271d8..acc60fbc3 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,7 @@ addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.4") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From 5d5a4c2e5d44f928fe3e5e6547bf8cc2c724ec51 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:13:48 +0200 Subject: [PATCH 049/311] Update scala3-library, ... to 3.3.4 (#1167) --- .github/workflows/ci.yml | 2 +- project/BuildHelper.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee02a60b1..e356c4296 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: fail-fast: false matrix: java: ['17', '21'] - scala: ['2.12.19', '2.13.13', '3.3.3'] + scala: ['2.12.19', '2.13.13', '3.3.4'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index abc9e8281..fc94a15d3 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -20,7 +20,7 @@ object BuildHelper { } val Scala212: String = versions("2.12") val Scala213: String = versions("2.13") - val ScalaDotty: String = "3.3.3" + val ScalaDotty: String = "3.3.4" val SilencerVersion = "1.7.16" From 7fc0ae8351e985a13deaf425870d97f68e5a66f2 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:45:22 +0200 Subject: [PATCH 050/311] Update auxlib, clib, javalib, nativelib, ... to 0.5.5 (#1148) Co-authored-by: Ferdinand Svehla --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index acc60fbc3..c218a8360 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.4") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.5") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") From c304ae5019efd8dec2462f651c82821081838fec Mon Sep 17 00:00:00 2001 From: Ferdinand Svehla Date: Mon, 21 Oct 2024 11:32:03 +0200 Subject: [PATCH 051/311] Update Scala, silencer, semanticdb (#1181) * CI: Scala 2.13.14 * Update silencer * Update semanticDB * set scala version to 2.13.15 * remove unnecessary @nowarn * no fatal warnings --- .github/workflows/ci.yml | 4 ++-- project/BuildHelper.scala | 7 ++++--- .../src/main/scala/zio/json/golden/package.scala | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e356c4296..fc4a19969 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: java: ['17', '21'] - scala: ['2.13.13'] + scala: ['2.13.15'] steps: - name: Checkout current branch uses: actions/checkout@v4.1.2 @@ -79,7 +79,7 @@ jobs: fail-fast: false matrix: java: ['17', '21'] - scala: ['2.12.19', '2.13.13', '3.3.4'] + scala: ['2.12.19', '2.13.15', '3.3.4'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index fc94a15d3..77ec4c3cf 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -22,7 +22,7 @@ object BuildHelper { val Scala213: String = versions("2.13") val ScalaDotty: String = "3.3.4" - val SilencerVersion = "1.7.16" + val SilencerVersion = "1.7.19" private val stdOptions = Seq( "-deprecation", @@ -32,7 +32,8 @@ object BuildHelper { "-unchecked" ) ++ { if (sys.env.contains("CI")) { - Seq("-Xfatal-warnings") + // Seq("-Xfatal-warnings") // enable this when we are ready to enforce this + Nil } else { Nil // to enable Scalafix locally } @@ -229,7 +230,7 @@ object BuildHelper { }, semanticdbEnabled := scalaVersion.value != ScalaDotty, // enable SemanticDB semanticdbOptions += "-P:semanticdb:synthetics:on", - semanticdbVersion := "4.9.2", + semanticdbVersion := "4.10.2", Test / parallelExecution := true, incOptions ~= (_.withLogRecompileOnMacro(false)), autoAPIMappings := true, diff --git a/zio-json-golden/src/main/scala/zio/json/golden/package.scala b/zio-json-golden/src/main/scala/zio/json/golden/package.scala index e36fcfc7c..8bdf6aec8 100644 --- a/zio-json-golden/src/main/scala/zio/json/golden/package.scala +++ b/zio-json-golden/src/main/scala/zio/json/golden/package.scala @@ -27,7 +27,7 @@ package object golden { else DiffResult.Different(x, y) } - @nowarn implicit private lazy val diff: Diff[GoldenSample] = (x: GoldenSample, y: GoldenSample) => + implicit private lazy val diff: Diff[GoldenSample] = (x: GoldenSample, y: GoldenSample) => Diff[Json].diff(x.samples, y.samples) def goldenTest[A: Tag: JsonEncoder]( From 4fceb6e22e4808383d04aaa559c1241d5531f48f Mon Sep 17 00:00:00 2001 From: Ferdinand Svehla Date: Mon, 21 Oct 2024 12:03:33 +0200 Subject: [PATCH 052/311] Update libraries (#1182) * HTTP4s: 0.23.28 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 93b9f7c52..db317f3f3 100644 --- a/build.sbt +++ b/build.sbt @@ -332,7 +332,7 @@ lazy val zioJsonInteropHttp4s = project .settings( crossScalaVersions -= ScalaDotty, libraryDependencies ++= Seq( - "org.http4s" %% "http4s-dsl" % "0.23.26", + "org.http4s" %% "http4s-dsl" % "0.23.28", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.4.9", "dev.zio" %% "zio-interop-cats" % "23.1.0.3" % "test", From 5e66917e267749a389204b66b07bcba67323e305 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:11:20 +0100 Subject: [PATCH 053/311] Update sbt, scripted-plugin to 1.10.4 (#1184) --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index 04267b14a..09feeeed5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.4 From ee7dd8244fca99e7576895b21afbee30bc860f25 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:11:33 +0100 Subject: [PATCH 054/311] Update http4s-dsl to 0.23.29 (#1183) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index db317f3f3..89a3a8eff 100644 --- a/build.sbt +++ b/build.sbt @@ -332,7 +332,7 @@ lazy val zioJsonInteropHttp4s = project .settings( crossScalaVersions -= ScalaDotty, libraryDependencies ++= Seq( - "org.http4s" %% "http4s-dsl" % "0.23.28", + "org.http4s" %% "http4s-dsl" % "0.23.29", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.4.9", "dev.zio" %% "zio-interop-cats" % "23.1.0.3" % "test", From 106658fed346f2d406eb2c6aa03403076b570fa7 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:18:25 +0100 Subject: [PATCH 055/311] Update sbt, scripted-plugin to 1.10.5 (#1186) --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index 09feeeed5..db1723b08 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.4 +sbt.version=1.10.5 From b7795b8586e53057c226249190d8ab5f234246e3 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:18:32 +0100 Subject: [PATCH 056/311] Update sbt-buildinfo to 0.13.0 (#1185) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index c218a8360..20f449927 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.12.0") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.0") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.6.1") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") From 9339fbbaa7fe2c88865e9b55d1a842cb5fd2db7f Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Fri, 13 Dec 2024 08:01:11 +0100 Subject: [PATCH 057/311] feat: implement JsonCodecConfiguration for scala 3 and explicitEmptyCollections for JsonCodecConfiguration (#1193) --- .../src/main/scala-2.x/zio/json/macros.scala | 71 +- .../zio/json/JsonCodecVersionSpecific.scala | 2 +- .../zio/json/JsonDecoderVersionSpecific.scala | 2 +- .../zio/json/JsonEncoderVersionSpecific.scala | 2 +- .../src/main/scala-3/zio/json/macros.scala | 60 +- .../zio/json/JsonCodecConfiguration.scala | 57 ++ .../src/main/scala/zio/json/JsonDecoder.scala | 211 ++++-- .../src/main/scala/zio/json/JsonEncoder.scala | 114 +++- .../json/ConfigurableDeriveCodecSpec.scala | 199 ------ .../json/ConfigurableDeriveCodecSpec.scala | 619 ++++++++++++++++++ 10 files changed, 983 insertions(+), 354 deletions(-) create mode 100644 zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala delete mode 100644 zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala create mode 100644 zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 42fffbb92..59b31d30f 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -2,8 +2,6 @@ package zio.json import magnolia1._ import zio.Chunk -import zio.json.JsonCodecConfiguration.SumTypeHandling -import zio.json.JsonCodecConfiguration.SumTypeHandling.WrapperWithClassNameField import zio.json.JsonDecoder.{ JsonError, UnsafeJson } import zio.json.ast.Json import zio.json.internal.{ Lexer, RetractReader, StringMatrix, Write } @@ -22,7 +20,8 @@ final case class jsonField(name: String) extends Annotation */ final case class jsonAliases(alias: String, aliases: String*) extends Annotation -final class jsonExplicitNull extends Annotation +final class jsonExplicitNull extends Annotation +final class jsonExplicitEmptyCollection extends Annotation /** * If used on a sealed class, will determine the name of the field for @@ -201,59 +200,6 @@ final class jsonNoExtraFields extends Annotation */ final class jsonExclude extends Annotation -// TODO: implement same configuration for Scala 3 once this issue is resolved: https://github.com/softwaremill/magnolia/issues/296 -/** - * Implicit codec derivation configuration. - * - * @param sumTypeHandling see [[jsonDiscriminator]] - * @param fieldNameMapping see [[jsonMemberNames]] - * @param allowExtraFields see [[jsonNoExtraFields]] - * @param sumTypeMapping see [[jsonHintNames]] - */ -final case class JsonCodecConfiguration( - sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, - fieldNameMapping: JsonMemberFormat = IdentityFormat, - allowExtraFields: Boolean = true, - sumTypeMapping: JsonMemberFormat = IdentityFormat, - explicitNulls: Boolean = false -) - -object JsonCodecConfiguration { - implicit val default: JsonCodecConfiguration = JsonCodecConfiguration() - - sealed trait SumTypeHandling { - def discriminatorField: Option[String] - } - - object SumTypeHandling { - - /** - * Use an object with a single key that is the class name. - */ - case object WrapperWithClassNameField extends SumTypeHandling { - override def discriminatorField: Option[String] = None - } - - /** - * For sealed classes, will determine the name of the field for - * disambiguating classes. - * - * The default is to not use a typehint field and instead - * have an object with a single key that is the class name. - * See [[WrapperWithClassNameField]]. - * - * Note that using a discriminator is less performant, uses more memory, and may - * be prone to DOS attacks that are impossible with the default encoding. In - * addition, there is slightly less type safety when using custom product - * encoders (which must write an unenforced object type). Only use this option - * if you must model an externally defined schema. - */ - final case class DiscriminatorField(name: String) extends SumTypeHandling { - override def discriminatorField: Option[String] = Some(name) - } - } -} - object DeriveJsonDecoder { type Typeclass[A] = JsonDecoder[A] @@ -561,8 +507,14 @@ object DeriveJsonEncoder { val explicitNulls: Boolean = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val explicitEmptyCollections: Boolean = + config.explicitEmptyCollections || ctx.annotations.exists(_.isInstanceOf[jsonExplicitEmptyCollection]) + lazy val tcs: Array[JsonEncoder[Any]] = params.map(p => p.typeclass.asInstanceOf[JsonEncoder[Any]]) val len: Int = params.length + + override def isEmpty(a: A): Boolean = params.forall(p => p.typeclass.isEmpty(p.dereference(a))) + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { var i = 0 out.write("{") @@ -574,7 +526,12 @@ object DeriveJsonEncoder { val tc = tcs(i) val p = params(i).dereference(a) val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) - if (!tc.isNothing(p) || writeNulls) { + val writeEmptyCollections = + explicitEmptyCollections || params(i).annotations.exists(_.isInstanceOf[jsonExplicitEmptyCollection]) + if ( + (!tc.isNothing(p) && !tc.isEmpty(p)) || (tc + .isNothing(p) && writeNulls) || (tc.isEmpty(p) && writeEmptyCollections) + ) { // if we have at least one field already, we need a comma if (prevFields) { if (indent.isEmpty) out.write(",") diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala index 4d309c805..f9c180f0a 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala @@ -1,6 +1,6 @@ package zio.json private[json] trait JsonCodecVersionSpecific { - inline def derived[A: deriving.Mirror.Of]: JsonCodec[A] = DeriveJsonCodec.gen[A] + inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonCodec[A] = DeriveJsonCodec.gen[A] } diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala index ad020b005..30bc02f1e 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala @@ -4,7 +4,7 @@ import scala.compiletime.* import scala.compiletime.ops.any.IsConst private[json] trait JsonDecoderVersionSpecific { - inline def derived[A: deriving.Mirror.Of]: JsonDecoder[A] = DeriveJsonDecoder.gen[A] + inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonDecoder[A] = DeriveJsonDecoder.gen[A] } trait DecoderLowPriorityVersionSpecific { diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala index 17f501c40..f0d14cf25 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala @@ -3,7 +3,7 @@ package zio.json import scala.compiletime.ops.any.IsConst private[json] trait JsonEncoderVersionSpecific { - inline def derived[A: deriving.Mirror.Of]: JsonEncoder[A] = DeriveJsonEncoder.gen[A] + inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonEncoder[A] = DeriveJsonEncoder.gen[A] } private[json] trait EncoderLowPriorityVersionSpecific { diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index f5f04c4c6..79b8cf3b6 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -32,6 +32,11 @@ final case class jsonAliases(alias: String, aliases: String*) extends Annotation */ final class jsonExplicitNull extends Annotation +/** + * Empty collections will be encoded as `null`. + */ +final class jsonExplicitEmptyCollection extends Annotation + /** * If used on a sealed class, will determine the name of the field for * disambiguating classes. @@ -225,19 +230,20 @@ private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) } } - -// TODO: implement same configuration as for Scala 2 once this issue is resolved: https://github.com/softwaremill/magnolia/issues/296 -object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => + +final class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonDecoder] { self => def join[A](ctx: CaseClass[Typeclass, A]): JsonDecoder[A] = { val (transformNames, nameTransform): (Boolean, String => String) = ctx.annotations.collectFirst { case jsonMemberNames(format) => format } + .orElse(Some(config.fieldNameMapping)) + .filter(_ != IdentityFormat) .map(true -> _) .getOrElse(false -> identity) val no_extra = ctx .annotations .collectFirst { case _: jsonNoExtraFields => () } - .isDefined + .isDefined || !config.allowExtraFields if (ctx.params.isEmpty) { new CaseObjectDecoder(ctx, no_extra) @@ -387,7 +393,7 @@ object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => def split[A](ctx: SealedTrait[JsonDecoder, A]): JsonDecoder[A] = { val jsonHintFormat: JsonMemberFormat = - ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(IdentityFormat) + ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val names: Array[String] = IArray.genericWrapArray(ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name @@ -407,7 +413,7 @@ object DeriveJsonDecoder extends Derivation[JsonDecoder] { self => !ctx.isEnum && ctx.subtypes.forall(_.isObject) ) - def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n } + def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) if (isEnumeration && discrim.isEmpty) { new JsonDecoder[A] { @@ -542,14 +548,23 @@ private lazy val caseObjectEncoder = new JsonEncoder[Any] { Right(Json.Obj(Chunk.empty)) } -object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => - def join[A](ctx: CaseClass[Typeclass, A]): JsonEncoder[A] = +object DeriveJsonDecoder { + inline def gen[A](using config: JsonCodecConfiguration, mirror: Mirror.Of[A]) = { + val derivation = new JsonDecoderDerivation(config) + derivation.derived[A] + } +} + +final class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonEncoder] { self => + def join[A](ctx: CaseClass[Typeclass, A]): JsonEncoder[A] = if (ctx.params.isEmpty) { caseObjectEncoder.narrow[A] } else { new JsonEncoder[A] { val (transformNames, nameTransform): (Boolean, String => String) = ctx.annotations.collectFirst { case jsonMemberNames(format) => format } + .orElse(Some(config.fieldNameMapping)) + .filter(_ != IdentityFormat) .map(true -> _) .getOrElse(false -> identity) @@ -575,7 +590,8 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => }) .toArray - val explicitNulls = ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val explicitEmptyCollections = config.explicitEmptyCollections || ctx.annotations.exists(_.isInstanceOf[jsonExplicitEmptyCollection]) lazy val tcs: Array[JsonEncoder[Any]] = IArray.genericWrapArray(params.map(_.typeclass.asInstanceOf[JsonEncoder[Any]])).toArray @@ -593,7 +609,11 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => val tc = tcs(i) val p = params(i).deref(a) val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) - if (! tc.isNothing(p) || writeNulls) { + val writeEmptyCollections = explicitEmptyCollections || params(i).annotations.exists(_.isInstanceOf[jsonExplicitEmptyCollection]) + if ( + (!tc.isNothing(p) && !tc.isEmpty(p)) || (tc + .isNothing(p) && writeNulls) || (tc.isEmpty(p) && writeEmptyCollections) + ) { // if we have at least one field already, we need a comma if (prevFields) { if (indent.isEmpty) { @@ -641,20 +661,20 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => } } - def split[A](ctx: SealedTrait[JsonEncoder, A]): JsonEncoder[A] = { + def split[A](ctx: SealedTrait[JsonEncoder, A]): JsonEncoder[A] = { val isEnumeration = (ctx.isEnum && ctx.subtypes.forall(_.typeclass == caseObjectEncoder)) || ( !ctx.isEnum && ctx.subtypes.forall(_.isObject) ) val jsonHintFormat: JsonMemberFormat = - ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(IdentityFormat) + ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val discrim = ctx .annotations .collectFirst { case jsonDiscriminator(n) => n - } + }.orElse(config.sumTypeHandling.discriminatorField) if (isEnumeration && discrim.isEmpty) { new JsonEncoder[A] { @@ -750,7 +770,7 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => JsonEncoder.string.unsafeEncode(getName(sub.annotations, sub.typeInfo.short), indent_, out) // whitespace is always off by 2 spaces at the end, probably not worth fixing - val intermediate = new NestedWriter(out, indent_) + val intermediate = new DeriveJsonEncoder.NestedWriter(out, indent_) sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) } } @@ -766,16 +786,20 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => } } } +} - inline def gen[A](using mirror: Mirror.Of[A]) = self.derived[A] +object DeriveJsonEncoder { + inline def gen[A](using config: JsonCodecConfiguration, mirror: Mirror.Of[A]) = { + val derivation = new JsonEncoderDerivation(config) + derivation.derived[A] + } // intercepts the first `{` of a nested writer and discards it. We also need to // inject a `,` unless an empty object `{}` has been written. - private[this] final class NestedWriter(out: Write, indent: Option[Int]) extends Write { + private[json] final class NestedWriter(out: Write, indent: Option[Int]) extends Write { private[this] var first, second = true def write(c: Char): Unit = write(c.toString) // could be optimised - def write(s: String): Unit = if (first || second) { var i = 0 @@ -798,7 +822,7 @@ object DeriveJsonEncoder extends Derivation[JsonEncoder] { self => } object DeriveJsonCodec { - inline def gen[A](using mirror: Mirror.Of[A]) = { + inline def gen[A](using mirror: Mirror.Of[A], config: JsonCodecConfiguration) = { val encoder = DeriveJsonEncoder.gen[A] val decoder = DeriveJsonDecoder.gen[A] diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala new file mode 100644 index 000000000..f6b31f688 --- /dev/null +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala @@ -0,0 +1,57 @@ +package zio.json + +import zio.json.JsonCodecConfiguration.SumTypeHandling +import zio.json.JsonCodecConfiguration.SumTypeHandling.WrapperWithClassNameField + +/** + * Implicit codec derivation configuration. + * + * @param sumTypeHandling see [[jsonDiscriminator]] + * @param fieldNameMapping see [[jsonMemberNames]] + * @param allowExtraFields see [[jsonNoExtraFields]] + * @param sumTypeMapping see [[jsonHintNames]] + */ +final case class JsonCodecConfiguration( + sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, + fieldNameMapping: JsonMemberFormat = IdentityFormat, + allowExtraFields: Boolean = true, + sumTypeMapping: JsonMemberFormat = IdentityFormat, + explicitNulls: Boolean = false, + explicitEmptyCollections: Boolean = true +) + +object JsonCodecConfiguration { + implicit val default: JsonCodecConfiguration = JsonCodecConfiguration() + + sealed trait SumTypeHandling { + def discriminatorField: Option[String] + } + + object SumTypeHandling { + + /** + * Use an object with a single key that is the class name. + */ + case object WrapperWithClassNameField extends SumTypeHandling { + override def discriminatorField: Option[String] = None + } + + /** + * For sealed classes, will determine the name of the field for + * disambiguating classes. + * + * The default is to not use a typehint field and instead + * have an object with a single key that is the class name. + * See [[WrapperWithClassNameField]]. + * + * Note that using a discriminator is less performant, uses more memory, and may + * be prone to DOS attacks that are impossible with the default encoding. In + * addition, there is slightly less type safety when using custom product + * encoders (which must write an unenforced object type). Only use this option + * if you must model an externally defined schema. + */ + final case class DiscriminatorField(name: String) extends SumTypeHandling { + override def discriminatorField: Option[String] = Some(name) + } + } +} diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 5be8a54ec..13ffe3de5 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -482,125 +482,216 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { this: JsonDecoder.type => - implicit def array[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[Array[A]] = new JsonDecoder[Array[A]] { + implicit def array[A: JsonDecoder: reflect.ClassTag](implicit config: JsonCodecConfiguration): JsonDecoder[Array[A]] = + new JsonDecoder[Array[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = - builder(trace, in, Array.newBuilder[A]) - } + override def unsafeDecodeMissing(trace: List[JsonError]): Array[A] = + if (!config.explicitEmptyCollections) Array.empty + else super.unsafeDecodeMissing(trace) - implicit def seq[A: JsonDecoder]: JsonDecoder[Seq[A]] = new JsonDecoder[Seq[A]] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = + builder(trace, in, Array.newBuilder[A]) + } - def unsafeDecode(trace: List[JsonError], in: RetractReader): Seq[A] = - builder(trace, in, immutable.Seq.newBuilder[A]) - } + implicit def seq[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Seq[A]] = + new JsonDecoder[Seq[A]] { - implicit def chunk[A: JsonDecoder]: JsonDecoder[Chunk[A]] = new JsonDecoder[Chunk[A]] { - val decoder = JsonDecoder[A] - def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[A] = - builder(trace, in, zio.ChunkBuilder.make[A]()) + override def unsafeDecodeMissing(trace: List[JsonError]): Seq[A] = + if (!config.explicitEmptyCollections) Seq.empty + else super.unsafeDecodeMissing(trace) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = - json match { - case Json.Arr(elements) => - elements.zipWithIndex.map { case (json, i) => - decoder.unsafeFromJsonAST(JsonError.ArrayAccess(i) :: trace, json) - } - case _ => throw UnsafeJson(JsonError.Message("Not an array") :: trace) - } - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Seq[A] = + builder(trace, in, immutable.Seq.newBuilder[A]) + } + + implicit def chunk[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Chunk[A]] = + new JsonDecoder[Chunk[A]] { + + override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[A] = + if (!config.explicitEmptyCollections) Chunk.empty + else super.unsafeDecodeMissing(trace) + + val decoder = JsonDecoder[A] + def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[A] = + builder(trace, in, zio.ChunkBuilder.make[A]()) + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = + json match { + case Json.Arr(elements) => + elements.zipWithIndex.map { case (json, i) => + decoder.unsafeFromJsonAST(JsonError.ArrayAccess(i) :: trace, json) + } + case _ => throw UnsafeJson(JsonError.Message("Not an array") :: trace) + } + } implicit def nonEmptyChunk[A: JsonDecoder]: JsonDecoder[NonEmptyChunk[A]] = chunk[A].mapOrFail(NonEmptyChunk.fromChunk(_).toRight("Chunk was empty")) - implicit def indexedSeq[A: JsonDecoder]: JsonDecoder[IndexedSeq[A]] = + implicit def indexedSeq[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[IndexedSeq[A]] = new JsonDecoder[IndexedSeq[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): IndexedSeq[A] = + if (!config.explicitEmptyCollections) IndexedSeq.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): IndexedSeq[A] = builder(trace, in, IndexedSeq.newBuilder[A]) } - implicit def linearSeq[A: JsonDecoder]: JsonDecoder[immutable.LinearSeq[A]] = + implicit def linearSeq[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[immutable.LinearSeq[A]] = new JsonDecoder[immutable.LinearSeq[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.LinearSeq[A] = + if (!config.explicitEmptyCollections) immutable.LinearSeq.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): LinearSeq[A] = builder(trace, in, immutable.LinearSeq.newBuilder[A]) } - implicit def listSet[A: JsonDecoder]: JsonDecoder[immutable.ListSet[A]] = new JsonDecoder[immutable.ListSet[A]] { + implicit def listSet[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[immutable.ListSet[A]] = + new JsonDecoder[immutable.ListSet[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): ListSet[A] = - builder(trace, in, immutable.ListSet.newBuilder[A]) - } + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListSet[A] = + if (!config.explicitEmptyCollections) immutable.ListSet.empty + else super.unsafeDecodeMissing(trace) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): ListSet[A] = + builder(trace, in, immutable.ListSet.newBuilder[A]) + } - implicit def treeSet[A: JsonDecoder: Ordering]: JsonDecoder[immutable.TreeSet[A]] = + implicit def treeSet[A: JsonDecoder: Ordering](implicit + config: JsonCodecConfiguration + ): JsonDecoder[immutable.TreeSet[A]] = new JsonDecoder[immutable.TreeSet[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.TreeSet[A] = + if (!config.explicitEmptyCollections) immutable.TreeSet.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): TreeSet[A] = builder(trace, in, immutable.TreeSet.newBuilder[A]) } - implicit def list[A: JsonDecoder]: JsonDecoder[List[A]] = new JsonDecoder[List[A]] { + implicit def list[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[List[A]] = + new JsonDecoder[List[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): List[A] = - builder(trace, in, new mutable.ListBuffer[A]) - } + override def unsafeDecodeMissing(trace: List[JsonError]): List[A] = + if (!config.explicitEmptyCollections) List.empty + else super.unsafeDecodeMissing(trace) - implicit def vector[A: JsonDecoder]: JsonDecoder[Vector[A]] = new JsonDecoder[Vector[A]] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): List[A] = + builder(trace, in, new mutable.ListBuffer[A]) + } - def unsafeDecode(trace: List[JsonError], in: RetractReader): Vector[A] = - builder(trace, in, immutable.Vector.newBuilder[A]) - } + implicit def vector[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Vector[A]] = + new JsonDecoder[Vector[A]] { - implicit def set[A: JsonDecoder]: JsonDecoder[Set[A]] = new JsonDecoder[Set[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): Vector[A] = + if (!config.explicitEmptyCollections) Vector.empty + else super.unsafeDecodeMissing(trace) - def unsafeDecode(trace: List[JsonError], in: RetractReader): Set[A] = - builder(trace, in, Set.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Vector[A] = + builder(trace, in, immutable.Vector.newBuilder[A]) + } - implicit def hashSet[A: JsonDecoder]: JsonDecoder[immutable.HashSet[A]] = new JsonDecoder[immutable.HashSet[A]] { + implicit def set[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Set[A]] = + new JsonDecoder[Set[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashSet[A] = - builder(trace, in, immutable.HashSet.newBuilder[A]) - } + override def unsafeDecodeMissing(trace: List[JsonError]): Set[A] = + if (!config.explicitEmptyCollections) Set.empty + else super.unsafeDecodeMissing(trace) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): Set[A] = + builder(trace, in, Set.newBuilder[A]) + } + + implicit def hashSet[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[immutable.HashSet[A]] = + new JsonDecoder[immutable.HashSet[A]] { + + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashSet[A] = + if (!config.explicitEmptyCollections) immutable.HashSet.empty + else super.unsafeDecodeMissing(trace) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashSet[A] = + builder(trace, in, immutable.HashSet.newBuilder[A]) + } - implicit def map[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[Map[K, V]] = + implicit def map[K: JsonFieldDecoder, V: JsonDecoder](implicit + config: JsonCodecConfiguration + ): JsonDecoder[Map[K, V]] = new JsonDecoder[Map[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): Map[K, V] = + if (!config.explicitEmptyCollections) Map.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): Map[K, V] = keyValueBuilder(trace, in, Map.newBuilder[K, V]) } - implicit def hashMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.HashMap[K, V]] = + implicit def hashMap[K: JsonFieldDecoder, V: JsonDecoder](implicit + config: JsonCodecConfiguration + ): JsonDecoder[immutable.HashMap[K, V]] = new JsonDecoder[immutable.HashMap[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashMap[K, V] = + if (!config.explicitEmptyCollections) immutable.HashMap.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashMap[K, V] = keyValueBuilder(trace, in, immutable.HashMap.newBuilder[K, V]) } - implicit def mutableMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[mutable.Map[K, V]] = + implicit def mutableMap[K: JsonFieldDecoder, V: JsonDecoder](implicit + config: JsonCodecConfiguration + ): JsonDecoder[mutable.Map[K, V]] = new JsonDecoder[mutable.Map[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): mutable.Map[K, V] = + if (!config.explicitEmptyCollections) mutable.Map.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): mutable.Map[K, V] = keyValueBuilder(trace, in, mutable.Map.newBuilder[K, V]) } - implicit def sortedSet[A: Ordering: JsonDecoder]: JsonDecoder[immutable.SortedSet[A]] = + implicit def sortedSet[A: Ordering: JsonDecoder](implicit + config: JsonCodecConfiguration + ): JsonDecoder[immutable.SortedSet[A]] = new JsonDecoder[immutable.SortedSet[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.SortedSet[A] = + if (!config.explicitEmptyCollections) immutable.SortedSet.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.SortedSet[A] = builder(trace, in, immutable.SortedSet.newBuilder[A]) } - implicit def sortedMap[K: JsonFieldDecoder: Ordering, V: JsonDecoder]: JsonDecoder[collection.SortedMap[K, V]] = + implicit def sortedMap[K: JsonFieldDecoder: Ordering, V: JsonDecoder](implicit + config: JsonCodecConfiguration + ): JsonDecoder[collection.SortedMap[K, V]] = new JsonDecoder[collection.SortedMap[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): collection.SortedMap[K, V] = + if (!config.explicitEmptyCollections) collection.SortedMap.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): collection.SortedMap[K, V] = keyValueBuilder(trace, in, collection.SortedMap.newBuilder[K, V]) } - implicit def listMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.ListMap[K, V]] = + implicit def listMap[K: JsonFieldDecoder, V: JsonDecoder](implicit + config: JsonCodecConfiguration + ): JsonDecoder[immutable.ListMap[K, V]] = new JsonDecoder[immutable.ListMap[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListMap[K, V] = + if (!config.explicitEmptyCollections) immutable.ListMap.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.ListMap[K, V] = keyValueBuilder(trace, in, immutable.ListMap.newBuilder[K, V]) } @@ -620,19 +711,29 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { private[json] trait DecoderLowPriority2 extends DecoderLowPriority3 { this: JsonDecoder.type => - implicit def iterable[A: JsonDecoder]: JsonDecoder[Iterable[A]] = new JsonDecoder[Iterable[A]] { + implicit def iterable[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Iterable[A]] = + new JsonDecoder[Iterable[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Iterable[A] = - builder(trace, in, immutable.Iterable.newBuilder[A]) - } + override def unsafeDecodeMissing(trace: List[JsonError]): Iterable[A] = + if (!config.explicitEmptyCollections) Iterable.empty + else super.unsafeDecodeMissing(trace) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): Iterable[A] = + builder(trace, in, immutable.Iterable.newBuilder[A]) + } // not implicit because this overlaps with decoders for lists of tuples def keyValueChunk[K, A](implicit K: JsonFieldDecoder[K], - A: JsonDecoder[A] + A: JsonDecoder[A], + config: JsonCodecConfiguration ): JsonDecoder[Chunk[(K, A)]] = new JsonDecoder[Chunk[(K, A)]] { + override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[(K, A)] = + if (!config.explicitEmptyCollections) Chunk.empty + else super.unsafeDecodeMissing(trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[(K, A)] = keyValueBuilder[K, A, ({ type lambda[X, Y] = Chunk[(X, Y)] })#lambda]( trace, diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 7098a1b83..44e00936b 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -39,6 +39,8 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { override def isNothing(b: B): Boolean = self.isNothing(f(b)) + override def isEmpty(b: B): Boolean = self.isEmpty(f(b)) + override final def toJsonAST(b: B): Either[String, Json] = self.toJsonAST(f(b)) } @@ -82,6 +84,12 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { */ def isNothing(a: A): Boolean = false + /** + * This default may be overridden when this value may be empty within a JSON object and still + * be encoded. + */ + def isEmpty(a: A): Boolean = false + /** * Returns this encoder but narrowed to the its given sub-type */ @@ -187,6 +195,8 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def isNothing(a: A): Boolean = encoder.isNothing(a) + override def isEmpty(a: A): Boolean = encoder.isEmpty(a) + override def toJsonAST(a: A): Either[String, Json] = encoder.toJsonAST(a) } @@ -296,8 +306,15 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { this: JsonEncoder.type => - implicit def array[A](implicit A: JsonEncoder[A], classTag: ClassTag[A]): JsonEncoder[Array[A]] = + implicit def array[A](implicit + A: JsonEncoder[A], + classTag: ClassTag[A], + config: JsonCodecConfiguration + ): JsonEncoder[Array[A]] = new JsonEncoder[Array[A]] { + + override def isEmpty(as: Array[A]): Boolean = as.isEmpty + def unsafeEncode(as: Array[A], indent: Option[Int], out: Write): Unit = if (as.isEmpty) out.write("[]") else { @@ -341,52 +358,92 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { .map(Json.Arr(_)) } - implicit def seq[A: JsonEncoder]: JsonEncoder[Seq[A]] = iterable[A, Seq] + implicit def seq[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[Seq[A]] = iterable[A, Seq] - implicit def chunk[A: JsonEncoder]: JsonEncoder[Chunk[A]] = iterable[A, Chunk] + implicit def chunk[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[Chunk[A]] = iterable[A, Chunk] implicit def nonEmptyChunk[A: JsonEncoder]: JsonEncoder[NonEmptyChunk[A]] = chunk[A].contramap(_.toChunk) - implicit def indexedSeq[A: JsonEncoder]: JsonEncoder[IndexedSeq[A]] = iterable[A, IndexedSeq] + implicit def indexedSeq[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[IndexedSeq[A]] = iterable[A, IndexedSeq] - implicit def linearSeq[A: JsonEncoder]: JsonEncoder[immutable.LinearSeq[A]] = iterable[A, immutable.LinearSeq] + implicit def linearSeq[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[immutable.LinearSeq[A]] = iterable[A, immutable.LinearSeq] - implicit def listSet[A: JsonEncoder]: JsonEncoder[immutable.ListSet[A]] = iterable[A, immutable.ListSet] + implicit def listSet[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[immutable.ListSet[A]] = iterable[A, immutable.ListSet] - implicit def treeSet[A: JsonEncoder]: JsonEncoder[immutable.TreeSet[A]] = iterable[A, immutable.TreeSet] + implicit def treeSet[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[immutable.TreeSet[A]] = iterable[A, immutable.TreeSet] - implicit def list[A: JsonEncoder]: JsonEncoder[List[A]] = iterable[A, List] + implicit def list[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[List[A]] = iterable[A, List] - implicit def vector[A: JsonEncoder]: JsonEncoder[Vector[A]] = iterable[A, Vector] + implicit def vector[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[Vector[A]] = iterable[A, Vector] - implicit def set[A: JsonEncoder]: JsonEncoder[Set[A]] = iterable[A, Set] + implicit def set[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[Set[A]] = + iterable[A, Set] - implicit def hashSet[A: JsonEncoder]: JsonEncoder[immutable.HashSet[A]] = iterable[A, immutable.HashSet] + implicit def hashSet[A: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[immutable.HashSet[A]] = + iterable[A, immutable.HashSet] - implicit def sortedSet[A: Ordering: JsonEncoder]: JsonEncoder[immutable.SortedSet[A]] = + implicit def sortedSet[A: Ordering: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[immutable.SortedSet[A]] = iterable[A, immutable.SortedSet] - implicit def map[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[Map[K, V]] = + implicit def map[K: JsonFieldEncoder, V: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[Map[K, V]] = keyValueIterable[K, V, Map] - implicit def hashMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[immutable.HashMap[K, V]] = + implicit def hashMap[K: JsonFieldEncoder, V: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[immutable.HashMap[K, V]] = keyValueIterable[K, V, immutable.HashMap] - implicit def mutableMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[mutable.Map[K, V]] = + implicit def mutableMap[K: JsonFieldEncoder, V: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[mutable.Map[K, V]] = keyValueIterable[K, V, mutable.Map] - implicit def sortedMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[collection.SortedMap[K, V]] = + implicit def sortedMap[K: JsonFieldEncoder, V: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[collection.SortedMap[K, V]] = keyValueIterable[K, V, collection.SortedMap] - implicit def listMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[immutable.ListMap[K, V]] = + implicit def listMap[K: JsonFieldEncoder, V: JsonEncoder](implicit + config: JsonCodecConfiguration + ): JsonEncoder[immutable.ListMap[K, V]] = keyValueIterable[K, V, immutable.ListMap] } private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { this: JsonEncoder.type => - implicit def iterable[A, T[X] <: Iterable[X]](implicit A: JsonEncoder[A]): JsonEncoder[T[A]] = + implicit def iterable[A, T[X] <: Iterable[X]](implicit + A: JsonEncoder[A], + config: JsonCodecConfiguration + ): JsonEncoder[T[A]] = new JsonEncoder[T[A]] { + + override def isEmpty(as: T[A]): Boolean = as.isEmpty + def unsafeEncode(as: T[A], indent: Option[Int], out: Write): Unit = if (as.isEmpty) out.write("[]") else { @@ -432,8 +489,12 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { // not implicit because this overlaps with encoders for lists of tuples def keyValueIterable[K, A, T[X, Y] <: Iterable[(X, Y)]](implicit K: JsonFieldEncoder[K], - A: JsonEncoder[A] + A: JsonEncoder[A], + config: JsonCodecConfiguration ): JsonEncoder[T[K, A]] = new JsonEncoder[T[K, A]] { + + override def isEmpty(a: T[K, A]): Boolean = a.isEmpty + def unsafeEncode(kvs: T[K, A], indent: Option[Int], out: Write): Unit = if (kvs.isEmpty) out.write("{}") else { @@ -447,7 +508,11 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { kvs.foreach { var first = true kv => - if (!A.isNothing(kv._2)) { + if ( + (!A.isNothing(kv._2) && !A.isEmpty(kv._2)) || (A + .isNothing(kv._2) && config.explicitNulls) || (A.isEmpty(kv._2) && config.explicitEmptyCollections) + ) { + // if (!A.isNothing(kv._2)) { if (first) first = false else out.write(',') string.unsafeEncode(K.unsafeEncodeField(kv._1), indent, out) @@ -462,7 +527,11 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { kvs.foreach { var first = true kv => - if (!A.isNothing(kv._2)) { + if ( + (!A.isNothing(kv._2) && !A.isEmpty(kv._2)) || (A + .isNothing(kv._2) && config.explicitNulls) || (A.isEmpty(kv._2) && config.explicitEmptyCollections) + ) { + // if (!A.isNothing(kv._2)) { if (first) first = false else { out.write(',') @@ -491,7 +560,8 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { // not implicit because this overlaps with encoders for lists of tuples def keyValueChunk[K, A](implicit K: JsonFieldEncoder[K], - A: JsonEncoder[A] + A: JsonEncoder[A], + config: JsonCodecConfiguration ): JsonEncoder[({ type lambda[X, Y] = Chunk[(X, Y)] })#lambda[K, A]] = keyValueIterable[K, A, ({ type lambda[X, Y] = Chunk[(X, Y)] })#lambda] } diff --git a/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala deleted file mode 100644 index 2cafc819f..000000000 --- a/zio-json/shared/src/test/scala-2.x/zio/json/ConfigurableDeriveCodecSpec.scala +++ /dev/null @@ -1,199 +0,0 @@ -package zio.json - -import zio.json.JsonCodecConfiguration.SumTypeHandling.DiscriminatorField -import zio.json.ast.Json -import zio.test._ - -object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { - case class ClassWithFields(someField: Int, someOtherField: String) - - sealed trait ST - - object ST { - case object CaseObj extends ST - case class CaseClass(i: Int) extends ST - } - - case class OptionalField(a: Option[Int]) - - def spec = suite("ConfigurableDeriveCodecSpec")( - suite("defaults")( - suite("string")( - test("should not map field names by default") { - val expectedStr = """{"someField":1,"someOtherField":"a"}""" - val expectedObj = ClassWithFields(1, "a") - - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("should not use discriminator by default") { - val expectedStr = """{"CaseObj":{}}""" - val expectedObj: ST = ST.CaseObj - - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[ST].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("should allow extra fields by default") { - val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" - val expectedObj = ClassWithFields(1, "a") - - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen - - assertTrue( - jsonStr.fromJson[ClassWithFields].toOption.get == expectedObj - ) - } - ), - suite("AST")( - test("should not map field names by default") { - val expectedAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a")) - val expectedObj = ClassWithFields(1, "a") - - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen - - assertTrue( - expectedAST.as[ClassWithFields].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST - ) - }, - test("should not use discriminator by default") { - val expectedAST = Json.Obj("CaseObj" -> Json.Obj()) - val expectedObj: ST = ST.CaseObj - - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen - - assertTrue( - expectedAST.as[ST].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST - ) - }, - test("should allow extra fields by default") { - val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) - val expectedObj = ClassWithFields(1, "a") - - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen - - assertTrue( - jsonAST.as[ClassWithFields].toOption.get == expectedObj - ) - } - ) - ), - suite("overrides")( - suite("string")( - test("should override field name mapping") { - val expectedStr = """{"some_field":1,"some_other_field":"a"}""" - val expectedObj = ClassWithFields(1, "a") - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(fieldNameMapping = SnakeCase) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("should specify discriminator") { - val expectedStr = """{"$type":"CaseClass","i":1}""" - val expectedObj: ST = ST.CaseClass(i = 1) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type")) - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[ST].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("should override sum type mapping") { - val expectedStr = """{"$type":"case_class","i":1}""" - val expectedObj: ST = ST.CaseClass(i = 1) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type"), sumTypeMapping = SnakeCase) - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[ST].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("should prevent extra fields") { - val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(allowExtraFields = false) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen - - assertTrue( - jsonStr.fromJson[ClassWithFields].isLeft - ) - } - ), - suite("AST")( - test("should override field name mapping") { - val expectedAST = Json.Obj("some_field" -> Json.Num(1), "some_other_field" -> Json.Str("a")) - val expectedObj = ClassWithFields(1, "a") - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(fieldNameMapping = SnakeCase) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen - - assertTrue( - expectedAST.as[ClassWithFields].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST - ) - }, - test("should specify discriminator") { - val expectedAST = Json.Obj("$type" -> Json.Str("CaseClass"), "i" -> Json.Num(1)) - val expectedObj: ST = ST.CaseClass(i = 1) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type")) - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen - - assertTrue( - expectedAST.as[ST].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST - ) - }, - test("should prevent extra fields") { - val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(allowExtraFields = false) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen - - assertTrue( - jsonAST.as[ClassWithFields].isLeft - ) - } - ) - ), - suite("explicit nulls")( - test("write null if configured") { - val expectedStr = """{"a":null}""" - val expectedObj = OptionalField(None) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitNulls = true) - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[OptionalField].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - } - ) - ) -} diff --git a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala new file mode 100644 index 000000000..a4224fbc2 --- /dev/null +++ b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala @@ -0,0 +1,619 @@ +package zio.json + +import zio.json.JsonCodecConfiguration.SumTypeHandling.DiscriminatorField +import zio.json.ast.Json +import zio.test._ +import zio.Chunk + +import scala.collection.immutable +import scala.collection.mutable + +object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { + case class ClassWithFields(someField: Int, someOtherField: String) + + sealed trait ST + + object ST { + case object CaseObj extends ST + case class CaseClass(i: Int) extends ST + } + + case class OptionalField(a: Option[Int]) + + def spec = suite("ConfigurableDeriveCodecSpec")( + suite("defaults")( + suite("string")( + test("should not map field names by default") { + val expectedStr = """{"someField":1,"someOtherField":"a"}""" + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should not use discriminator by default") { + val expectedStr = """{"CaseObj":{}}""" + val expectedObj: ST = ST.CaseObj + + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should allow extra fields by default") { + val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonStr.fromJson[ClassWithFields].toOption.get == expectedObj + ) + } + ), + suite("AST")( + test("should not map field names by default") { + val expectedAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a")) + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should not use discriminator by default") { + val expectedAST = Json.Obj("CaseObj" -> Json.Obj()) + val expectedObj: ST = ST.CaseObj + + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ST].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should allow extra fields by default") { + val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonAST.as[ClassWithFields].toOption.get == expectedObj + ) + } + ) + ), + suite("overrides")( + suite("string")( + test("should override field name mapping") { + val expectedStr = """{"some_field":1,"some_other_field":"a"}""" + val expectedObj = ClassWithFields(1, "a") + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(fieldNameMapping = SnakeCase) + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should specify discriminator") { + val expectedStr = """{"$type":"CaseClass","i":1}""" + val expectedObj: ST = ST.CaseClass(i = 1) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type")) + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should override sum type mapping") { + val expectedStr = """{"$type":"case_class","i":1}""" + val expectedObj: ST = ST.CaseClass(i = 1) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type"), sumTypeMapping = SnakeCase) + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should prevent extra fields") { + val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(allowExtraFields = false) + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonStr.fromJson[ClassWithFields].isLeft + ) + } + ), + suite("AST")( + test("should override field name mapping") { + val expectedAST = Json.Obj("some_field" -> Json.Num(1), "some_other_field" -> Json.Str("a")) + val expectedObj = ClassWithFields(1, "a") + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(fieldNameMapping = SnakeCase) + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should specify discriminator") { + val expectedAST = Json.Obj("$type" -> Json.Str("CaseClass"), "i" -> Json.Num(1)) + val expectedObj: ST = ST.CaseClass(i = 1) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type")) + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ST].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should prevent extra fields") { + val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(allowExtraFields = false) + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonAST.as[ClassWithFields].isLeft + ) + } + ) + ), + suite("explicit nulls")( + test("write null if configured") { + val expectedStr = """{"a":null}""" + val expectedObj = OptionalField(None) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitNulls = true) + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + } + ), + suite("explicit empty collections")( + suite("should write empty collections if set to true")( + test("for an array") { + case class EmptyArray(a: Array[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyArray(Array.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyArray].toOption.get.a.isEmpty, expectedObj.toJson == expectedStr) + }, + test("for a seq") { + case class EmptySeq(a: Seq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySeq(Seq.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a chunk") { + case class EmptyChunk(a: Chunk[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyChunk(Chunk.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyChunk].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for an indexed seq") { + case class EmptyIndexedSeq(a: IndexedSeq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyIndexedSeq].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a linear seq") { + case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyLinearSeq].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a list set") { + case class EmptyListSet(a: immutable.ListSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyListSet(immutable.ListSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a tree set") { + case class EmptyTreeSet(a: immutable.TreeSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyTreeSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a list") { + case class EmptyList(a: List[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyList(List.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyList].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a vector") { + case class EmptyVector(a: Vector[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyVector(Vector.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyVector].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a set") { + case class EmptySet(a: Set[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySet(Set.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a hash set") { + case class EmptyHashSet(a: immutable.HashSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyHashSet(immutable.HashSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a sorted set") { + case class EmptySortedSet(a: immutable.SortedSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySortedSet(immutable.SortedSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptySortedSet].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a map") { + case class EmptyMap(a: Map[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyMap(Map.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a hash map") { + case class EmptyHashMap(a: immutable.HashMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyHashMap(immutable.HashMap.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a mutable map") { + case class EmptyMutableMap(a: mutable.Map[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyMutableMap(mutable.Map.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyMutableMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a sorted map") { + case class EmptySortedMap(a: collection.SortedMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptySortedMap(collection.SortedMap.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptySortedMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a list map") { + case class EmptyListMap(a: immutable.ListMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyListMap(immutable.ListMap.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = true) + implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + } + ), + suite("should not write empty collections if set to false")( + test("for an array") { + case class EmptyArray(a: Array[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyArray(Array.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyArray].toOption.get.a.isEmpty, expectedObj.toJson == expectedStr) + }, + test("for a seq") { + case class EmptySeq(a: Seq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySeq(Seq.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a chunk") { + case class EmptyChunk(a: Chunk[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyChunk(Chunk.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyChunk].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for an indexed seq") { + case class EmptyIndexedSeq(a: IndexedSeq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyIndexedSeq].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a linear seq") { + case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyLinearSeq].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a list set") { + case class EmptyListSet(a: immutable.ListSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyListSet(immutable.ListSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a treeSet") { + case class EmptyTreeSet(a: immutable.TreeSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyTreeSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a list") { + case class EmptyList(a: List[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyList(List.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyList].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a vector") { + case class EmptyVector(a: Vector[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyVector(Vector.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyVector].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a set") { + case class EmptySet(a: Set[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySet(Set.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a hash set") { + case class EmptyHashSet(a: immutable.HashSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyHashSet(immutable.HashSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a sorted set") { + case class EmptySortedSet(a: immutable.SortedSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySortedSet(immutable.SortedSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptySortedSet].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a map") { + case class EmptyMap(a: Map[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyMap(Map.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a hashMap") { + case class EmptyHashMap(a: immutable.HashMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyHashMap(immutable.HashMap.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a mutable map") { + case class EmptyMutableMap(a: mutable.Map[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyMutableMap(mutable.Map.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyMutableMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a sorted map") { + case class EmptySortedMap(a: collection.SortedMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptySortedMap(collection.SortedMap.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptySortedMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a list map") { + case class EmptyListMap(a: immutable.ListMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyListMap(immutable.ListMap.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + } + ) + ) + ) +} From f4c3bc25d4868972968163aab0692f89626960f0 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Mon, 16 Dec 2024 06:36:24 +0100 Subject: [PATCH 058/311] How to decode a specific enum/sealed trait subtype with discriminator (#1112) Co-authored-by: Milad Khajavi --- docs/configuration.md | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 87631ca8b..ffcbf6cc0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -55,8 +55,8 @@ sealed trait Fruit ) extends Fruit object Fruit { - implicit val encoder: JsonEncoder[Fruit] = - DeriveJsonEncoder.gen[Fruit] + implicit val codec: JsonCodec[Fruit] = + DeriveJsonCodec.gen[Fruit] } val banana: Fruit = Banana(0.5) @@ -96,8 +96,8 @@ case class GoodFruit(good: Boolean) extends FruitKind case class BadFruit(bad: Boolean) extends FruitKind object FruitKind { - implicit val encoder: JsonEncoder[FruitKind] = - DeriveJsonEncoder.gen[FruitKind] + implicit val codec: JsonCodec[FruitKind] = + DeriveJsonCodec.gen[FruitKind] } val goodFruit: FruitKind = GoodFruit(true) @@ -106,6 +106,34 @@ val badFruit: FruitKind = BadFruit(true) goodFruit.toJson badFruit.toJson ``` + +Note that with this code, you can't directly decode the subclasses of `FruitKind`. You would need to create a dedicated decoder for each subclass. + +```scala mdoc +object GoodFruit { + implicit val codec: JsonCodec[GoodFruit] = + DeriveJsonCodec.gen[GoodFruit] +} +``` + +Since `GoodFruit` is only a case class, it will not require any kind of discriminator to be decoded. + +```scala mdoc +"""{"good":true}""".fromJson[GoodFruit] +``` + +If you want for some reason to decode only for a specific type of `FruitKind` that has a discriminator, don't derive the codec for the subtype, but transform the `FruitKind` codec. + +```scala mdoc +object BadFruit { + implicit val decoder: JsonDecoder[BadFruit] = + FruitKind.codec.decoder.mapOrFail { + case GoodFruit(_) => Left("Expected BadFruit, got GoodFruit") + case BadFruit(bad) => Right(BadFruit(bad)) + } +} +``` + ## jsonDiscriminator From 5341e99a7c5ef8fcd2ea2463ccd68f919c2951b4 Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Sat, 28 Dec 2024 07:10:23 +0100 Subject: [PATCH 059/311] fix unused explicitEmptyCollection annotation in macros and AST encoding (#1196) --- .github/workflows/site.yml | 2 +- .../src/main/scala-2.x/zio/json/macros.scala | 26 +- .../src/main/scala-3/zio/json/macros.scala | 42 +- .../src/main/scala/zio/json/JsonDecoder.scala | 118 ++-- .../src/main/scala/zio/json/JsonEncoder.scala | 88 +-- .../scala/zio/json/AnnotationsCodecSpec.scala | 559 ++++++++++++++++++ .../json/ConfigurableDeriveCodecSpec.scala | 107 +++- 7 files changed, 762 insertions(+), 180 deletions(-) create mode 100644 zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 01cbaa3df..8b33f0ba2 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -14,7 +14,7 @@ name: Website jobs: build: name: Build and Test - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 if: ${{ github.event_name == 'pull_request' }} steps: - name: Git Checkout diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 59b31d30f..73de220c9 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -20,8 +20,12 @@ final case class jsonField(name: String) extends Annotation */ final case class jsonAliases(alias: String, aliases: String*) extends Annotation -final class jsonExplicitNull extends Annotation -final class jsonExplicitEmptyCollection extends Annotation +final class jsonExplicitNull extends Annotation + +/** + * When disabled keys with empty collections will be omitted from the JSON. + */ +final case class jsonExplicitEmptyCollection(enabled: Boolean = true) extends Annotation /** * If used on a sealed class, will determine the name of the field for @@ -508,7 +512,9 @@ object DeriveJsonEncoder { config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) val explicitEmptyCollections: Boolean = - config.explicitEmptyCollections || ctx.annotations.exists(_.isInstanceOf[jsonExplicitEmptyCollection]) + ctx.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => + enabled + }.getOrElse(config.explicitEmptyCollections) lazy val tcs: Array[JsonEncoder[Any]] = params.map(p => p.typeclass.asInstanceOf[JsonEncoder[Any]]) val len: Int = params.length @@ -527,7 +533,9 @@ object DeriveJsonEncoder { val p = params(i).dereference(a) val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) val writeEmptyCollections = - explicitEmptyCollections || params(i).annotations.exists(_.isInstanceOf[jsonExplicitEmptyCollection]) + params(i).annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => + enabled + }.getOrElse(explicitEmptyCollections) if ( (!tc.isNothing(p) && !tc.isEmpty(p)) || (tc .isNothing(p) && writeNulls) || (tc.isEmpty(p) && writeEmptyCollections) @@ -558,9 +566,17 @@ object DeriveJsonEncoder { val name = param.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(param.label)) + val writeNulls = explicitNulls || param.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val writeEmptyCollections = + param.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => + enabled + }.getOrElse(explicitEmptyCollections) c.flatMap { chunk => param.typeclass.toJsonAST(param.dereference(a)).map { value => - if (value == Json.Null) chunk + if ( + (value == Json.Null && !writeNulls) || + (value.asObject.exists(_.fields.isEmpty) && !writeEmptyCollections) + ) chunk else chunk :+ name -> value } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 79b8cf3b6..672b52cd8 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -33,9 +33,9 @@ final case class jsonAliases(alias: String, aliases: String*) extends Annotation final class jsonExplicitNull extends Annotation /** - * Empty collections will be encoded as `null`. + * When disabled keys with empty collections will be omitted from the JSON. */ -final class jsonExplicitEmptyCollection extends Annotation +final case class jsonExplicitEmptyCollection(enabled: Boolean = true) extends Annotation /** * If used on a sealed class, will determine the name of the field for @@ -58,6 +58,7 @@ final case class jsonDiscriminator(name: String) extends Annotation // Subtype. sealed trait JsonMemberFormat extends (String => String) + case class CustomCase(f: String => String) extends JsonMemberFormat { override def apply(memberName: String): String = f(memberName) } @@ -68,6 +69,7 @@ case object CamelCase extends JsonMemberFormat { override def apply(memberName: String): String = jsonMemberNames.enforceCamelOrPascalCase(memberName, toPascal = false) } + case object PascalCase extends JsonMemberFormat { override def apply(memberName: String): String = jsonMemberNames.enforceCamelOrPascalCase(memberName, toPascal = true) } @@ -135,9 +137,9 @@ private[json] object jsonMemberNames { } def enforceSnakeOrKebabCase(s: String, separator: Char): String = { - val len = s.length - val sb = new StringBuilder(len << 1) - var i = 0 + val len = s.length + val sb = new StringBuilder(len << 1) + var i = 0 var isPrecedingNotUpperCased = false while (i < len) isPrecedingNotUpperCased = { val ch = s.charAt(i) @@ -158,9 +160,9 @@ private[json] object jsonMemberNames { } def enforceSnakeOrKebabCaseSeparateNumbers(s: String, separator: Char): String = { - val len = s.length - val sb = new StringBuilder(len << 1) - var i = 0 + val len = s.length + val sb = new StringBuilder(len << 1) + var i = 0 var isPrecedingLowerCased = false while (i < len) isPrecedingLowerCased = { val ch = s.charAt(i) @@ -591,7 +593,10 @@ final class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriva .toArray val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - val explicitEmptyCollections = config.explicitEmptyCollections || ctx.annotations.exists(_.isInstanceOf[jsonExplicitEmptyCollection]) + val explicitEmptyCollections = + ctx.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => + enabled + }.getOrElse(config.explicitEmptyCollections) lazy val tcs: Array[JsonEncoder[Any]] = IArray.genericWrapArray(params.map(_.typeclass.asInstanceOf[JsonEncoder[Any]])).toArray @@ -606,10 +611,13 @@ final class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriva var prevFields = false while (i < len) { - val tc = tcs(i) - val p = params(i).deref(a) + val tc = tcs(i) + val p = params(i).deref(a) val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) - val writeEmptyCollections = explicitEmptyCollections || params(i).annotations.exists(_.isInstanceOf[jsonExplicitEmptyCollection]) + val writeEmptyCollections = + params(i).annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => + enabled + }.getOrElse(explicitEmptyCollections) if ( (!tc.isNothing(p) && !tc.isEmpty(p)) || (tc .isNothing(p) && writeNulls) || (tc.isEmpty(p) && writeEmptyCollections) @@ -649,9 +657,17 @@ final class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriva val name = param.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(param.label)) + val writeNulls = explicitNulls || param.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val writeEmptyCollections = + param.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => + enabled + }.getOrElse(explicitEmptyCollections) c.flatMap { chunk => param.typeclass.toJsonAST(param.deref(a)).map { value => - if (value == Json.Null) chunk + if ( + (value == Json.Null && !writeNulls) || + (value.asObject.exists(_.fields.isEmpty) && !writeEmptyCollections) + ) chunk else chunk :+ name -> value } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 13ffe3de5..c3c68ea21 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -482,34 +482,29 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { this: JsonDecoder.type => - implicit def array[A: JsonDecoder: reflect.ClassTag](implicit config: JsonCodecConfiguration): JsonDecoder[Array[A]] = + implicit def array[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[Array[A]] = new JsonDecoder[Array[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Array[A] = - if (!config.explicitEmptyCollections) Array.empty - else super.unsafeDecodeMissing(trace) + override def unsafeDecodeMissing(trace: List[JsonError]): Array[A] = Array.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = builder(trace, in, Array.newBuilder[A]) } - implicit def seq[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Seq[A]] = + implicit def seq[A: JsonDecoder]: JsonDecoder[Seq[A]] = new JsonDecoder[Seq[A]] { override def unsafeDecodeMissing(trace: List[JsonError]): Seq[A] = - if (!config.explicitEmptyCollections) Seq.empty - else super.unsafeDecodeMissing(trace) + Seq.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Seq[A] = builder(trace, in, immutable.Seq.newBuilder[A]) } - implicit def chunk[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Chunk[A]] = + implicit def chunk[A: JsonDecoder]: JsonDecoder[Chunk[A]] = new JsonDecoder[Chunk[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[A] = - if (!config.explicitEmptyCollections) Chunk.empty - else super.unsafeDecodeMissing(trace) + override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[A] = Chunk.empty val decoder = JsonDecoder[A] def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[A] = @@ -528,169 +523,136 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def nonEmptyChunk[A: JsonDecoder]: JsonDecoder[NonEmptyChunk[A]] = chunk[A].mapOrFail(NonEmptyChunk.fromChunk(_).toRight("Chunk was empty")) - implicit def indexedSeq[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[IndexedSeq[A]] = + implicit def indexedSeq[A: JsonDecoder]: JsonDecoder[IndexedSeq[A]] = new JsonDecoder[IndexedSeq[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): IndexedSeq[A] = - if (!config.explicitEmptyCollections) IndexedSeq.empty - else super.unsafeDecodeMissing(trace) + override def unsafeDecodeMissing(trace: List[JsonError]): IndexedSeq[A] = IndexedSeq.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): IndexedSeq[A] = builder(trace, in, IndexedSeq.newBuilder[A]) } - implicit def linearSeq[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[immutable.LinearSeq[A]] = + implicit def linearSeq[A: JsonDecoder]: JsonDecoder[immutable.LinearSeq[A]] = new JsonDecoder[immutable.LinearSeq[A]] { override def unsafeDecodeMissing(trace: List[JsonError]): immutable.LinearSeq[A] = - if (!config.explicitEmptyCollections) immutable.LinearSeq.empty - else super.unsafeDecodeMissing(trace) + immutable.LinearSeq.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): LinearSeq[A] = builder(trace, in, immutable.LinearSeq.newBuilder[A]) } - implicit def listSet[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[immutable.ListSet[A]] = + implicit def listSet[A: JsonDecoder]: JsonDecoder[immutable.ListSet[A]] = new JsonDecoder[immutable.ListSet[A]] { override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListSet[A] = - if (!config.explicitEmptyCollections) immutable.ListSet.empty - else super.unsafeDecodeMissing(trace) + immutable.ListSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): ListSet[A] = builder(trace, in, immutable.ListSet.newBuilder[A]) } - implicit def treeSet[A: JsonDecoder: Ordering](implicit - config: JsonCodecConfiguration - ): JsonDecoder[immutable.TreeSet[A]] = + implicit def treeSet[A: JsonDecoder: Ordering]: JsonDecoder[immutable.TreeSet[A]] = new JsonDecoder[immutable.TreeSet[A]] { override def unsafeDecodeMissing(trace: List[JsonError]): immutable.TreeSet[A] = - if (!config.explicitEmptyCollections) immutable.TreeSet.empty - else super.unsafeDecodeMissing(trace) + immutable.TreeSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): TreeSet[A] = builder(trace, in, immutable.TreeSet.newBuilder[A]) } - implicit def list[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[List[A]] = + implicit def list[A: JsonDecoder]: JsonDecoder[List[A]] = new JsonDecoder[List[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): List[A] = - if (!config.explicitEmptyCollections) List.empty - else super.unsafeDecodeMissing(trace) + override def unsafeDecodeMissing(trace: List[JsonError]): List[A] = List.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): List[A] = builder(trace, in, new mutable.ListBuffer[A]) } - implicit def vector[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Vector[A]] = + implicit def vector[A: JsonDecoder]: JsonDecoder[Vector[A]] = new JsonDecoder[Vector[A]] { override def unsafeDecodeMissing(trace: List[JsonError]): Vector[A] = - if (!config.explicitEmptyCollections) Vector.empty - else super.unsafeDecodeMissing(trace) + Vector.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Vector[A] = builder(trace, in, immutable.Vector.newBuilder[A]) } - implicit def set[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Set[A]] = + implicit def set[A: JsonDecoder]: JsonDecoder[Set[A]] = new JsonDecoder[Set[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Set[A] = - if (!config.explicitEmptyCollections) Set.empty - else super.unsafeDecodeMissing(trace) + override def unsafeDecodeMissing(trace: List[JsonError]): Set[A] = Set.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Set[A] = builder(trace, in, Set.newBuilder[A]) } - implicit def hashSet[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[immutable.HashSet[A]] = + implicit def hashSet[A: JsonDecoder]: JsonDecoder[immutable.HashSet[A]] = new JsonDecoder[immutable.HashSet[A]] { override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashSet[A] = - if (!config.explicitEmptyCollections) immutable.HashSet.empty - else super.unsafeDecodeMissing(trace) + immutable.HashSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashSet[A] = builder(trace, in, immutable.HashSet.newBuilder[A]) } - implicit def map[K: JsonFieldDecoder, V: JsonDecoder](implicit - config: JsonCodecConfiguration - ): JsonDecoder[Map[K, V]] = + implicit def map[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[Map[K, V]] = new JsonDecoder[Map[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Map[K, V] = - if (!config.explicitEmptyCollections) Map.empty - else super.unsafeDecodeMissing(trace) + override def unsafeDecodeMissing(trace: List[JsonError]): Map[K, V] = Map.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Map[K, V] = keyValueBuilder(trace, in, Map.newBuilder[K, V]) } - implicit def hashMap[K: JsonFieldDecoder, V: JsonDecoder](implicit - config: JsonCodecConfiguration - ): JsonDecoder[immutable.HashMap[K, V]] = + implicit def hashMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.HashMap[K, V]] = new JsonDecoder[immutable.HashMap[K, V]] { override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashMap[K, V] = - if (!config.explicitEmptyCollections) immutable.HashMap.empty - else super.unsafeDecodeMissing(trace) + immutable.HashMap.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashMap[K, V] = keyValueBuilder(trace, in, immutable.HashMap.newBuilder[K, V]) } - implicit def mutableMap[K: JsonFieldDecoder, V: JsonDecoder](implicit - config: JsonCodecConfiguration - ): JsonDecoder[mutable.Map[K, V]] = + implicit def mutableMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[mutable.Map[K, V]] = new JsonDecoder[mutable.Map[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): mutable.Map[K, V] = - if (!config.explicitEmptyCollections) mutable.Map.empty - else super.unsafeDecodeMissing(trace) + override def unsafeDecodeMissing(trace: List[JsonError]): mutable.Map[K, V] = mutable.Map.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): mutable.Map[K, V] = keyValueBuilder(trace, in, mutable.Map.newBuilder[K, V]) } - implicit def sortedSet[A: Ordering: JsonDecoder](implicit - config: JsonCodecConfiguration - ): JsonDecoder[immutable.SortedSet[A]] = + implicit def sortedSet[A: Ordering: JsonDecoder]: JsonDecoder[immutable.SortedSet[A]] = new JsonDecoder[immutable.SortedSet[A]] { override def unsafeDecodeMissing(trace: List[JsonError]): immutable.SortedSet[A] = - if (!config.explicitEmptyCollections) immutable.SortedSet.empty - else super.unsafeDecodeMissing(trace) + immutable.SortedSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.SortedSet[A] = builder(trace, in, immutable.SortedSet.newBuilder[A]) } - implicit def sortedMap[K: JsonFieldDecoder: Ordering, V: JsonDecoder](implicit - config: JsonCodecConfiguration - ): JsonDecoder[collection.SortedMap[K, V]] = + implicit def sortedMap[K: JsonFieldDecoder: Ordering, V: JsonDecoder]: JsonDecoder[collection.SortedMap[K, V]] = new JsonDecoder[collection.SortedMap[K, V]] { override def unsafeDecodeMissing(trace: List[JsonError]): collection.SortedMap[K, V] = - if (!config.explicitEmptyCollections) collection.SortedMap.empty - else super.unsafeDecodeMissing(trace) + collection.SortedMap.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): collection.SortedMap[K, V] = keyValueBuilder(trace, in, collection.SortedMap.newBuilder[K, V]) } - implicit def listMap[K: JsonFieldDecoder, V: JsonDecoder](implicit - config: JsonCodecConfiguration - ): JsonDecoder[immutable.ListMap[K, V]] = + implicit def listMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.ListMap[K, V]] = new JsonDecoder[immutable.ListMap[K, V]] { override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListMap[K, V] = - if (!config.explicitEmptyCollections) immutable.ListMap.empty - else super.unsafeDecodeMissing(trace) + immutable.ListMap.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.ListMap[K, V] = keyValueBuilder(trace, in, immutable.ListMap.newBuilder[K, V]) @@ -711,12 +673,10 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { private[json] trait DecoderLowPriority2 extends DecoderLowPriority3 { this: JsonDecoder.type => - implicit def iterable[A: JsonDecoder](implicit config: JsonCodecConfiguration): JsonDecoder[Iterable[A]] = + implicit def iterable[A: JsonDecoder]: JsonDecoder[Iterable[A]] = new JsonDecoder[Iterable[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Iterable[A] = - if (!config.explicitEmptyCollections) Iterable.empty - else super.unsafeDecodeMissing(trace) + override def unsafeDecodeMissing(trace: List[JsonError]): Iterable[A] = Iterable.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Iterable[A] = builder(trace, in, immutable.Iterable.newBuilder[A]) @@ -725,14 +685,12 @@ private[json] trait DecoderLowPriority2 extends DecoderLowPriority3 { // not implicit because this overlaps with decoders for lists of tuples def keyValueChunk[K, A](implicit K: JsonFieldDecoder[K], - A: JsonDecoder[A], - config: JsonCodecConfiguration + A: JsonDecoder[A] ): JsonDecoder[Chunk[(K, A)]] = new JsonDecoder[Chunk[(K, A)]] { override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[(K, A)] = - if (!config.explicitEmptyCollections) Chunk.empty - else super.unsafeDecodeMissing(trace) + Chunk.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[(K, A)] = keyValueBuilder[K, A, ({ type lambda[X, Y] = Chunk[(X, Y)] })#lambda]( diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 44e00936b..66d179576 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -308,8 +308,7 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { implicit def array[A](implicit A: JsonEncoder[A], - classTag: ClassTag[A], - config: JsonCodecConfiguration + classTag: ClassTag[A] ): JsonEncoder[Array[A]] = new JsonEncoder[Array[A]] { @@ -358,78 +357,46 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { .map(Json.Arr(_)) } - implicit def seq[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[Seq[A]] = iterable[A, Seq] + implicit def seq[A: JsonEncoder]: JsonEncoder[Seq[A]] = iterable[A, Seq] - implicit def chunk[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[Chunk[A]] = iterable[A, Chunk] + implicit def chunk[A: JsonEncoder]: JsonEncoder[Chunk[A]] = iterable[A, Chunk] implicit def nonEmptyChunk[A: JsonEncoder]: JsonEncoder[NonEmptyChunk[A]] = chunk[A].contramap(_.toChunk) - implicit def indexedSeq[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[IndexedSeq[A]] = iterable[A, IndexedSeq] + implicit def indexedSeq[A: JsonEncoder]: JsonEncoder[IndexedSeq[A]] = iterable[A, IndexedSeq] - implicit def linearSeq[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[immutable.LinearSeq[A]] = iterable[A, immutable.LinearSeq] + implicit def linearSeq[A: JsonEncoder]: JsonEncoder[immutable.LinearSeq[A]] = iterable[A, immutable.LinearSeq] - implicit def listSet[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[immutable.ListSet[A]] = iterable[A, immutable.ListSet] + implicit def listSet[A: JsonEncoder]: JsonEncoder[immutable.ListSet[A]] = iterable[A, immutable.ListSet] - implicit def treeSet[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[immutable.TreeSet[A]] = iterable[A, immutable.TreeSet] + implicit def treeSet[A: JsonEncoder]: JsonEncoder[immutable.TreeSet[A]] = iterable[A, immutable.TreeSet] - implicit def list[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[List[A]] = iterable[A, List] + implicit def list[A: JsonEncoder]: JsonEncoder[List[A]] = iterable[A, List] - implicit def vector[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[Vector[A]] = iterable[A, Vector] + implicit def vector[A: JsonEncoder]: JsonEncoder[Vector[A]] = iterable[A, Vector] - implicit def set[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[Set[A]] = + implicit def set[A: JsonEncoder]: JsonEncoder[Set[A]] = iterable[A, Set] - implicit def hashSet[A: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[immutable.HashSet[A]] = + implicit def hashSet[A: JsonEncoder]: JsonEncoder[immutable.HashSet[A]] = iterable[A, immutable.HashSet] - implicit def sortedSet[A: Ordering: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[immutable.SortedSet[A]] = + implicit def sortedSet[A: Ordering: JsonEncoder]: JsonEncoder[immutable.SortedSet[A]] = iterable[A, immutable.SortedSet] - implicit def map[K: JsonFieldEncoder, V: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[Map[K, V]] = + implicit def map[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[Map[K, V]] = keyValueIterable[K, V, Map] - implicit def hashMap[K: JsonFieldEncoder, V: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[immutable.HashMap[K, V]] = + implicit def hashMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[immutable.HashMap[K, V]] = keyValueIterable[K, V, immutable.HashMap] - implicit def mutableMap[K: JsonFieldEncoder, V: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[mutable.Map[K, V]] = + implicit def mutableMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[mutable.Map[K, V]] = keyValueIterable[K, V, mutable.Map] - implicit def sortedMap[K: JsonFieldEncoder, V: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[collection.SortedMap[K, V]] = + implicit def sortedMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[collection.SortedMap[K, V]] = keyValueIterable[K, V, collection.SortedMap] - implicit def listMap[K: JsonFieldEncoder, V: JsonEncoder](implicit - config: JsonCodecConfiguration - ): JsonEncoder[immutable.ListMap[K, V]] = + implicit def listMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[immutable.ListMap[K, V]] = keyValueIterable[K, V, immutable.ListMap] } @@ -437,8 +404,7 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { this: JsonEncoder.type => implicit def iterable[A, T[X] <: Iterable[X]](implicit - A: JsonEncoder[A], - config: JsonCodecConfiguration + A: JsonEncoder[A] ): JsonEncoder[T[A]] = new JsonEncoder[T[A]] { @@ -489,8 +455,7 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { // not implicit because this overlaps with encoders for lists of tuples def keyValueIterable[K, A, T[X, Y] <: Iterable[(X, Y)]](implicit K: JsonFieldEncoder[K], - A: JsonEncoder[A], - config: JsonCodecConfiguration + A: JsonEncoder[A] ): JsonEncoder[T[K, A]] = new JsonEncoder[T[K, A]] { override def isEmpty(a: T[K, A]): Boolean = a.isEmpty @@ -508,11 +473,7 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { kvs.foreach { var first = true kv => - if ( - (!A.isNothing(kv._2) && !A.isEmpty(kv._2)) || (A - .isNothing(kv._2) && config.explicitNulls) || (A.isEmpty(kv._2) && config.explicitEmptyCollections) - ) { - // if (!A.isNothing(kv._2)) { + if (!A.isNothing(kv._2)) { if (first) first = false else out.write(',') string.unsafeEncode(K.unsafeEncodeField(kv._1), indent, out) @@ -527,11 +488,7 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { kvs.foreach { var first = true kv => - if ( - (!A.isNothing(kv._2) && !A.isEmpty(kv._2)) || (A - .isNothing(kv._2) && config.explicitNulls) || (A.isEmpty(kv._2) && config.explicitEmptyCollections) - ) { - // if (!A.isNothing(kv._2)) { + if (!A.isNothing(kv._2)) { if (first) first = false else { out.write(',') @@ -560,8 +517,7 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { // not implicit because this overlaps with encoders for lists of tuples def keyValueChunk[K, A](implicit K: JsonFieldEncoder[K], - A: JsonEncoder[A], - config: JsonCodecConfiguration + A: JsonEncoder[A] ): JsonEncoder[({ type lambda[X, Y] = Chunk[(X, Y)] })#lambda[K, A]] = keyValueIterable[K, A, ({ type lambda[X, Y] = Chunk[(X, Y)] })#lambda] } diff --git a/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala new file mode 100644 index 000000000..e669376b3 --- /dev/null +++ b/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala @@ -0,0 +1,559 @@ +package zio.json + +import zio.json.JsonCodecConfiguration.SumTypeHandling.DiscriminatorField +import zio.json.ast.Json +import zio.test._ +import zio.Chunk + +import scala.collection.immutable +import scala.collection.mutable + +object AnnotationsCodecSpec extends ZIOSpecDefault { + + def spec = suite("ConfigurableDeriveCodecSpec")( + suite("annotations overrides")( + suite("string")( + test("should override field name mapping") { + @jsonMemberNames(SnakeCase) + case class ClassWithFields(someField: Int, someOtherField: String) + + val expectedStr = """{"some_field":1,"some_other_field":"a"}""" + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should specify discriminator") { + @jsonDiscriminator("$type") + sealed trait ST + + object ST { + case object CaseObj extends ST + case class CaseClass(i: Int) extends ST + + implicit lazy val codec: JsonCodec[ST] = DeriveJsonCodec.gen + } + + val expectedStr = """{"$type":"CaseClass","i":1}""" + val expectedObj: ST = ST.CaseClass(i = 1) + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should override sum type mapping") { + @jsonHintNames(SnakeCase) + @jsonDiscriminator("$type") + sealed trait ST + + object ST { + case object CaseObj extends ST + case class CaseClass(i: Int) extends ST + + implicit lazy val codec: JsonCodec[ST] = DeriveJsonCodec.gen + } + + val expectedStr = """{"$type":"case_class","i":1}""" + val expectedObj: ST = ST.CaseClass(i = 1) + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should prevent extra fields") { + @jsonNoExtraFields + case class ClassWithFields(someField: Int, someOtherField: String) + + val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonStr.fromJson[ClassWithFields].isLeft + ) + }, + test("use explicit null values") { + @jsonExplicitNull + case class OptionalField(a: Option[Int]) + + val expectedStr = """{"a":null}""" + val expectedObj = OptionalField(None) + + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("do not write empty collections") { + @jsonExplicitEmptyCollection(false) + case class EmptySeq(a: Seq[Int]) + + val expectedStr = """{}""" + val expectedObj = EmptySeq(Seq.empty) + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + } + ), + suite("AST")( + test("should override field name mapping") { + @jsonMemberNames(SnakeCase) + case class ClassWithFields(someField: Int, someOtherField: String) + + val expectedAST = Json.Obj("some_field" -> Json.Num(1), "some_other_field" -> Json.Str("a")) + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should specify discriminator") { + @jsonDiscriminator("$type") + sealed trait ST + + object ST { + case object CaseObj extends ST + case class CaseClass(i: Int) extends ST + + implicit lazy val codec: JsonCodec[ST] = DeriveJsonCodec.gen + } + + val expectedAST = Json.Obj("$type" -> Json.Str("CaseClass"), "i" -> Json.Num(1)) + val expectedObj: ST = ST.CaseClass(i = 1) + + assertTrue( + expectedAST.as[ST].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should prevent extra fields") { + @jsonNoExtraFields + case class ClassWithFields(someField: Int, someOtherField: String) + + val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonAST.as[ClassWithFields].isLeft + ) + }, + test("use explicit null values") { + @jsonExplicitNull + case class OptionalField(a: Option[Int]) + + val jsonAST = Json.Obj("a" -> Json.Null) + val expectedObj = OptionalField(None) + + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue(jsonAST.as[OptionalField].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) + }, + test("do not write empty collections") { + @jsonExplicitEmptyCollection(false) + case class EmptySeq(a: Seq[Int]) + + val jsonAST = Json.Obj("a" -> Json.Arr()) + val expectedObj = EmptySeq(Seq.empty) + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(jsonAST.as[EmptySeq].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) + } + ) + ), + suite("explicit empty collections")( + suite("should write empty collections if set to true")( + test("for an array") { + @jsonExplicitEmptyCollection(true) + case class EmptyArray(a: Array[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyArray(Array.empty) + + implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyArray].toOption.get.a.isEmpty, expectedObj.toJson == expectedStr) + }, + test("for a seq") { + @jsonExplicitEmptyCollection(true) + case class EmptySeq(a: Seq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySeq(Seq.empty) + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a chunk") { + @jsonExplicitEmptyCollection(true) + case class EmptyChunk(a: Chunk[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyChunk(Chunk.empty) + + implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyChunk].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for an indexed seq") { + case class EmptyIndexedSeq(a: IndexedSeq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) + + implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyIndexedSeq].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a linear seq") { + @jsonExplicitEmptyCollection(true) + case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) + + implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyLinearSeq].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a list set") { + @jsonExplicitEmptyCollection(true) + case class EmptyListSet(a: immutable.ListSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyListSet(immutable.ListSet.empty) + + implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a tree set") { + @jsonExplicitEmptyCollection(true) + case class EmptyTreeSet(a: immutable.TreeSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) + + implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyTreeSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a list") { + @jsonExplicitEmptyCollection(true) + case class EmptyList(a: List[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyList(List.empty) + + implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyList].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a vector") { + @jsonExplicitEmptyCollection(true) + case class EmptyVector(a: Vector[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyVector(Vector.empty) + + implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyVector].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a set") { + @jsonExplicitEmptyCollection(true) + case class EmptySet(a: Set[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySet(Set.empty) + + implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a hash set") { + @jsonExplicitEmptyCollection(true) + case class EmptyHashSet(a: immutable.HashSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyHashSet(immutable.HashSet.empty) + + implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a sorted set") { + @jsonExplicitEmptyCollection(true) + case class EmptySortedSet(a: immutable.SortedSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySortedSet(immutable.SortedSet.empty) + + implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptySortedSet].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a map") { + @jsonExplicitEmptyCollection(true) + case class EmptyMap(a: Map[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyMap(Map.empty) + + implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a hash map") { + @jsonExplicitEmptyCollection(true) + case class EmptyHashMap(a: immutable.HashMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyHashMap(immutable.HashMap.empty) + + implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a mutable map") { + @jsonExplicitEmptyCollection(true) + case class EmptyMutableMap(a: mutable.Map[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyMutableMap(mutable.Map.empty) + + implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyMutableMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a sorted map") { + @jsonExplicitEmptyCollection(true) + case class EmptySortedMap(a: collection.SortedMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptySortedMap(collection.SortedMap.empty) + + implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptySortedMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a list map") { + @jsonExplicitEmptyCollection(true) + case class EmptyListMap(a: immutable.ListMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyListMap(immutable.ListMap.empty) + + implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + } + ), + suite("should not write empty collections if set to false")( + test("for an array") { + @jsonExplicitEmptyCollection(false) + case class EmptyArray(a: Array[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyArray(Array.empty) + + implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyArray].toOption.get.a.isEmpty, expectedObj.toJson == expectedStr) + }, + test("for a seq") { + @jsonExplicitEmptyCollection(false) + case class EmptySeq(a: Seq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySeq(Seq.empty) + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a chunk") { + @jsonExplicitEmptyCollection(false) + case class EmptyChunk(a: Chunk[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyChunk(Chunk.empty) + + implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyChunk].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for an indexed seq") { + @jsonExplicitEmptyCollection(false) + case class EmptyIndexedSeq(a: IndexedSeq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) + + implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyIndexedSeq].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a linear seq") { + @jsonExplicitEmptyCollection(false) + case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) + + implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyLinearSeq].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a list set") { + @jsonExplicitEmptyCollection(false) + case class EmptyListSet(a: immutable.ListSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyListSet(immutable.ListSet.empty) + + implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a treeSet") { + @jsonExplicitEmptyCollection(false) + case class EmptyTreeSet(a: immutable.TreeSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) + + implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyTreeSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a list") { + @jsonExplicitEmptyCollection(false) + case class EmptyList(a: List[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyList(List.empty) + + implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyList].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a vector") { + @jsonExplicitEmptyCollection(false) + case class EmptyVector(a: Vector[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyVector(Vector.empty) + + implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyVector].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a set") { + @jsonExplicitEmptyCollection(false) + case class EmptySet(a: Set[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySet(Set.empty) + + implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a hash set") { + @jsonExplicitEmptyCollection(false) + case class EmptyHashSet(a: immutable.HashSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyHashSet(immutable.HashSet.empty) + + implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a sorted set") { + @jsonExplicitEmptyCollection(false) + case class EmptySortedSet(a: immutable.SortedSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySortedSet(immutable.SortedSet.empty) + + implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptySortedSet].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a map") { + @jsonExplicitEmptyCollection(false) + case class EmptyMap(a: Map[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyMap(Map.empty) + + implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a hashMap") { + @jsonExplicitEmptyCollection(false) + case class EmptyHashMap(a: immutable.HashMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyHashMap(immutable.HashMap.empty) + + implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("for a mutable map") { + @jsonExplicitEmptyCollection(false) + case class EmptyMutableMap(a: mutable.Map[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyMutableMap(mutable.Map.empty) + + implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptyMutableMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a sorted map") { + @jsonExplicitEmptyCollection(false) + case class EmptySortedMap(a: collection.SortedMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptySortedMap(collection.SortedMap.empty) + + implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptySortedMap].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("for a list map") { + @jsonExplicitEmptyCollection(false) + case class EmptyListMap(a: immutable.ListMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyListMap(immutable.ListMap.empty) + + implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + } + ) + ) + ) +} diff --git a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala index a4224fbc2..75671f80a 100644 --- a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala @@ -56,6 +56,27 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { ) } ), + test("do not write nulls by default") { + val expectedStr = """{}""" + val expectedObj = OptionalField(None) + + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("write empty collections by default") { + case class EmptySeq(a: Seq[Int]) + + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySeq(Seq.empty) + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, suite("AST")( test("should not map field names by default") { val expectedAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a")) @@ -88,6 +109,30 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { assertTrue( jsonAST.as[ClassWithFields].toOption.get == expectedObj ) + }, + test("do not write nulls by default") { + val jsonAST = Json.Obj() + val expectedObj = OptionalField(None) + + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + jsonAST.as[OptionalField].toOption.get == expectedObj, + expectedObj.toJsonAST == Right(jsonAST) + ) + }, + test("write empty collections by default") { + case class EmptySeq(a: Seq[Int]) + + val jsonAST = Json.Obj("a" -> Json.Arr()) + val expectedObj = EmptySeq(Seq.empty) + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue( + jsonAST.as[EmptySeq].toOption.get == expectedObj, + expectedObj.toJsonAST == Right(jsonAST) + ) } ) ), @@ -142,6 +187,31 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { assertTrue( jsonStr.fromJson[ClassWithFields].isLeft ) + }, + test("use explicit null values") { + val expectedStr = """{"a":null}""" + val expectedObj = OptionalField(None) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitNulls = true) + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("do not write empty collections") { + case class EmptySeq(a: Seq[Int]) + + val expectedStr = """{}""" + val expectedObj = EmptySeq(Seq.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) } ), suite("AST")( @@ -181,23 +251,30 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { assertTrue( jsonAST.as[ClassWithFields].isLeft ) - } - ) - ), - suite("explicit nulls")( - test("write null if configured") { - val expectedStr = """{"a":null}""" - val expectedObj = OptionalField(None) + }, + test("use explicit null values") { + val jsonAST = Json.Obj("a" -> Json.Null) + val expectedObj = OptionalField(None) - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitNulls = true) - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitNulls = true) + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen - assertTrue( - expectedStr.fromJson[OptionalField].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - } + assertTrue(jsonAST.as[OptionalField].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) + }, + test("do not write empty collections") { + case class EmptySeq(a: Seq[Int]) + + val jsonAST = Json.Obj("a" -> Json.Arr()) + val expectedObj = EmptySeq(Seq.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = false) + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(jsonAST.as[EmptySeq].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) + } + ) ), suite("explicit empty collections")( suite("should write empty collections if set to true")( From 4d53d08db3c499278d263e1a08cf09a18cf88dc5 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 5 Jan 2025 15:01:51 +0100 Subject: [PATCH 060/311] Reduce instantiation time and memory footprint for all implementations of `JsonEncoder` trait (#1207) --- .../src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala b/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala index 3502c7270..60b7d15ff 100644 --- a/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala +++ b/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala @@ -74,9 +74,9 @@ trait JsonEncoderPlatformSpecific[A] { self: JsonEncoder[A] => } } - final val encodeJsonLinesPipeline: ZPipeline[Any, Throwable, A, Char] = + final lazy val encodeJsonLinesPipeline: ZPipeline[Any, Throwable, A, Char] = encodeJsonDelimitedPipeline(None, Some('\n'), None) - final val encodeJsonArrayPipeline: ZPipeline[Any, Throwable, A, Char] = + final lazy val encodeJsonArrayPipeline: ZPipeline[Any, Throwable, A, Char] = encodeJsonDelimitedPipeline(Some('['), Some(','), Some(']')) } From cb8474b3acc5517af55022367ba18d4cae3a3e19 Mon Sep 17 00:00:00 2001 From: kyri-petrou <67301607+kyri-petrou@users.noreply.github.com> Date: Sun, 5 Jan 2025 16:02:38 +0200 Subject: [PATCH 061/311] Update `scalafmt` version and run for Scala 3 (#1206) --- .github/workflows/ci.yml | 2 +- .scalafmt.conf | 19 +++++- build.sbt | 10 +-- project/BuildHelper.scala | 31 +++++----- project/NeoJmhPlugin.scala | 22 +++---- .../scala/zio/json/golden/filehelpers.scala | 2 +- .../zio/json/interop/refined/package.scala | 2 +- .../src/main/scala/zio/json/jsonDerive.scala | 3 +- .../scala/zio/json/yaml/YamlOptions.scala | 3 +- .../zio/json/GoogleMapsAPIBenchmarks.scala | 4 +- .../scala/zio/json/SyntheticBenchmarks.scala | 2 +- .../json/internal/SafeNumbersBenchmarks.scala | 6 +- .../json/JsonDecoderPlatformSpecific.scala | 14 +++-- .../src/test/scala/zio/json/TestUtils.scala | 6 +- .../src/main/scala-2.x/zio/json/macros.scala | 47 ++++++-------- .../zio/json/JsonDecoderVersionSpecific.scala | 13 ++-- .../zio/json/JsonEncoderVersionSpecific.scala | 3 +- .../scala-3/zio/json/union_derivation.scala | 2 +- .../src/main/scala/zio/json/JsonCodec.scala | 15 ++--- .../zio/json/JsonCodecConfiguration.scala | 27 ++++---- .../src/main/scala/zio/json/JsonDecoder.scala | 45 +++++++------- .../src/main/scala/zio/json/JsonEncoder.scala | 41 ++++++------- .../src/main/scala/zio/json/JsonError.scala | 4 +- .../src/main/scala/zio/json/ast/ast.scala | 61 +++++++++++-------- .../scala/zio/json/codegen/Generator.scala | 3 +- .../scala/zio/json/internal/numbers.scala | 38 ++++++------ .../scala/zio/json/internal/readers.scala | 15 ++--- .../scala/zio/json/javatime/serializers.scala | 6 +- .../src/main/scala/zio/json/package.scala | 8 +-- .../scala-3/zio/json/DerivedCodecSpec.scala | 2 +- .../scala-3/zio/json/DerivedDecoderSpec.scala | 6 +- 31 files changed, 227 insertions(+), 235 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc4a19969..880ed5ce6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - name: Cache scala dependencies uses: coursier/cache-action@v6 - name: Lint code - run: sbt check + run: sbt "++2.12; check; ++2.13; check; ++3.3; check" benchmarks: runs-on: ubuntu-20.04 diff --git a/.scalafmt.conf b/.scalafmt.conf index 46233e07a..1b52c7a5d 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,10 +1,11 @@ -version = "2.7.5" +version = "3.8.2" +runner.dialect = scala213 maxColumn = 120 align.preset = most align.multiline = false continuationIndent.defnSite = 2 assumeStandardLibraryStripMargin = true -docstrings = JavaDoc +docstrings.style = Asterisk lineEndings = preserve includeCurlyBraceInSelectChains = false danglingParentheses.preset = true @@ -26,3 +27,17 @@ rewriteTokens = { "→": "->" "←": "<-" } + +project.excludePaths = [ + "glob:**/target/**" + "glob:**/resources/**" +] + +fileOverride { + "glob:**/scala-3/**" { + runner.dialect = scala3 + } + "glob:**/project/**" { + runner.dialect = scala3 + } +} diff --git a/build.sbt b/build.sbt index 89a3a8eff..eed3b12a6 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -import BuildHelper._ +import BuildHelper.* import explicitdeps.ExplicitDepsPlugin.autoImport.moduleFilterRemoveValue import sbtcrossproject.CrossPlugin.autoImport.crossProject @@ -7,8 +7,8 @@ Global / onChangedBuildSource := IgnoreSourceChanges inThisBuild( List( organization := "dev.zio", - homepage := Some(url("https://zio.dev/zio-json/")), - licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), + homepage := Some(url("https://zio.dev/zio-json/")), + licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), developers := List( Developer( "jdegoes", @@ -390,9 +390,9 @@ lazy val docs = project crossScalaVersions -= ScalaDotty, moduleName := "zio-json-docs", scalacOptions += "-Ymacro-annotations", - projectName := "ZIO JSON", + projectName := "ZIO JSON", mainModuleName := (zioJsonJVM / moduleName).value, - projectStage := ProjectStage.ProductionReady, + projectStage := ProjectStage.ProductionReady, ScalaUnidoc / unidoc / unidocProjectFilter := inProjects( zioJsonJVM, zioJsonYaml, diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 77ec4c3cf..fddccb722 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -1,16 +1,17 @@ -import explicitdeps.ExplicitDepsPlugin.autoImport._ -import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ -import sbt.Keys._ -import sbt._ -import sbtbuildinfo.BuildInfoKeys._ -import sbtbuildinfo._ -import sbtcrossproject.CrossPlugin.autoImport._ +import explicitdeps.ExplicitDepsPlugin.autoImport.* +import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport.* +import sbt.* +import sbt.Keys.* +import sbtbuildinfo.* +import sbtbuildinfo.BuildInfoKeys.* +import sbtcrossproject.CrossPlugin.autoImport.* object BuildHelper { private val versions: Map[String, String] = { import org.snakeyaml.engine.v2.api.{ Load, LoadSettings } - import java.util.{ List => JList, Map => JMap } - import scala.jdk.CollectionConverters._ + + import java.util.{ List as JList, Map as JMap } + import scala.jdk.CollectionConverters.* val doc = new Load(LoadSettings.builder().build()) .loadFromReader(scala.io.Source.fromFile(".github/workflows/ci.yml").bufferedReader()) @@ -59,7 +60,7 @@ object BuildHelper { def buildInfoSettings(packageName: String) = Seq( - buildInfoKeys := Seq[BuildInfoKey](organization, moduleName, name, version, scalaVersion, sbtVersion, isSnapshot), + buildInfoKeys := Seq[BuildInfoKey](organization, moduleName, name, version, scalaVersion, sbtVersion, isSnapshot), buildInfoPackage := packageName ) @@ -212,8 +213,8 @@ object BuildHelper { ) def stdSettings(prjName: String) = Seq( - name := s"$prjName", - crossScalaVersions := Seq(Scala212, Scala213, ScalaDotty), + name := s"$prjName", + crossScalaVersions := Seq(Scala212, Scala213, ScalaDotty), ThisBuild / scalaVersion := Scala213, scalacOptions ++= stdOptions ++ extraOptions(scalaVersion.value, optimize = !isSnapshot.value), libraryDependencies ++= { @@ -230,7 +231,7 @@ object BuildHelper { }, semanticdbEnabled := scalaVersion.value != ScalaDotty, // enable SemanticDB semanticdbOptions += "-P:semanticdb:synthetics:on", - semanticdbVersion := "4.10.2", + semanticdbVersion := "4.10.2", Test / parallelExecution := true, incOptions ~= (_.withLogRecompileOnMacro(false)), autoAPIMappings := true, @@ -274,8 +275,8 @@ object BuildHelper { ) def nativeSettings = Seq( - Test / skip := true, - doc / skip := true, + Test / skip := true, + doc / skip := true, Compile / doc / sources := Seq.empty ) diff --git a/project/NeoJmhPlugin.scala b/project/NeoJmhPlugin.scala index a831c348b..bc5fd6e81 100644 --- a/project/NeoJmhPlugin.scala +++ b/project/NeoJmhPlugin.scala @@ -1,9 +1,8 @@ package fommil -import sbt._ -import sbt.Keys._ +import sbt.* +import sbt.Keys.* -import scala.collection.immutable.Set import scala.util.Try object NeoJmhKeys { @@ -27,11 +26,10 @@ object NeoJmhKeys { } /** - * https://github.com/ktoso/sbt-jmh/ rewritten as an idiomatic sbt - * Configuration (not requiring a separate Project). + * https://github.com/ktoso/sbt-jmh/ rewritten as an idiomatic sbt Configuration (not requiring a separate Project). */ object NeoJmhPlugin extends AutoPlugin { - import NeoJmhKeys._ + import NeoJmhKeys.* val autoImport = NeoJmhKeys val JmhInternal = (config("jmh-internal") extend Test).hide @@ -45,16 +43,16 @@ object NeoJmhPlugin extends AutoPlugin { override def projectConfigurations = Seq(Jmh, JmhInternal) override def buildSettings = Seq( - jmhVersion := "1.36", + jmhVersion := "1.36", jmhExtrasVersion := "0.3.7" ) override def projectSettings = inConfig(Jmh)( Defaults.testSettings ++ Seq( - run := (JmhInternal / run).evaluated, + run := (JmhInternal / run).evaluated, neoJmhGenerator := "reflection", - neoJmhYourkit := Nil, + neoJmhYourkit := Nil, javaOptions ++= Seq( "-XX:+PerfDisableSharedMem", "-XX:+AlwaysPreTouch", @@ -71,10 +69,10 @@ object NeoJmhPlugin extends AutoPlugin { ) ) ++ inConfig(JmhInternal)( Defaults.testSettings ++ Seq( - javaOptions := (Jmh / javaOptions).value, - envVars := (Jmh / envVars).value, + javaOptions := (Jmh / javaOptions).value, + envVars := (Jmh / envVars).value, run / mainClass := Some("org.openjdk.jmh.Main"), - run / fork := true, + run / fork := true, dependencyClasspath ++= (Jmh / fullClasspath).value, sourceGenerators += generateJmhSourcesAndResources.map { case (sources, _) => sources diff --git a/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala b/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala index 21a57da4b..15c1a2243 100644 --- a/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala +++ b/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala @@ -1,7 +1,7 @@ package zio.json.golden import java.io.{ File, IOException } -import java.nio.file.{ Path } +import java.nio.file.Path import zio.{ test => _, _ } import zio.json._ diff --git a/zio-json-interop-refined/shared/src/main/scala/zio/json/interop/refined/package.scala b/zio-json-interop-refined/shared/src/main/scala/zio/json/interop/refined/package.scala index ba558edc8..65d874923 100644 --- a/zio-json-interop-refined/shared/src/main/scala/zio/json/interop/refined/package.scala +++ b/zio-json-interop-refined/shared/src/main/scala/zio/json/interop/refined/package.scala @@ -1,7 +1,7 @@ package zio.json.interop import eu.timepit.refined.api.{ Refined, Validate } -import eu.timepit.refined.{ refineV } +import eu.timepit.refined.refineV import zio.json._ package object refined { diff --git a/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala b/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala index 8c35c9994..ed7dc1f80 100644 --- a/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala +++ b/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala @@ -74,12 +74,11 @@ private[json] final class DeriveCodecMacros(val c: blackbox.Context) { private[this] val EncoderClass = typeOf[JsonEncoder[_]].typeSymbol.asType private[this] val CodecClass = typeOf[JsonCodec[_]].typeSymbol.asType - private[this] val macroName: Tree = { + private[this] val macroName: Tree = c.prefix.tree match { case Apply(Select(New(name), _), _) => name case _ => c.abort(c.enclosingPosition, "Unexpected macro application") } - } private[this] val (codecStyle: JsonCodecStyle, codecType: JsonCodecType) = { val style: JsonCodecStyle = macroName match { diff --git a/zio-json-yaml/src/main/scala/zio/json/yaml/YamlOptions.scala b/zio-json-yaml/src/main/scala/zio/json/yaml/YamlOptions.scala index df4940b46..957aa6a7b 100644 --- a/zio-json-yaml/src/main/scala/zio/json/yaml/YamlOptions.scala +++ b/zio-json-yaml/src/main/scala/zio/json/yaml/YamlOptions.scala @@ -20,11 +20,10 @@ case class YamlOptions( ) object YamlOptions { - private val defaultLineBreak: LineBreak = { + private val defaultLineBreak: LineBreak = Set(LineBreak.MAC, LineBreak.WIN, LineBreak.UNIX) .find(_.getString == System.lineSeparator()) .getOrElse(LineBreak.UNIX) - } val default: YamlOptions = YamlOptions( () => new DumperOptions(), diff --git a/zio-json/jvm/src/jmh/scala/zio/json/GoogleMapsAPIBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/GoogleMapsAPIBenchmarks.scala index 277455ad1..d8e994145 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/GoogleMapsAPIBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/GoogleMapsAPIBenchmarks.scala @@ -62,8 +62,8 @@ class GoogleMapsAPIBenchmarks { @Setup def setup(): Unit = { - //Distance Matrix API call for top-10 by population cities in US: - //https://maps.googleapis.com/maps/api/distancematrix/json?origins=New+York|Los+Angeles|Chicago|Houston|Phoenix+AZ|Philadelphia|San+Antonio|San+Diego|Dallas|San+Jose&destinations=New+York|Los+Angeles|Chicago|Houston|Phoenix+AZ|Philadelphia|San+Antonio|San+Diego|Dallas|San+Jose + // Distance Matrix API call for top-10 by population cities in US: + // https://maps.googleapis.com/maps/api/distancematrix/json?origins=New+York|Los+Angeles|Chicago|Houston|Phoenix+AZ|Philadelphia|San+Antonio|San+Diego|Dallas|San+Jose&destinations=New+York|Los+Angeles|Chicago|Houston|Phoenix+AZ|Philadelphia|San+Antonio|San+Diego|Dallas|San+Jose jsonString = getResourceAsString("google_maps_api_response.json") jsonChars = asChars(jsonString) jsonStringCompact = getResourceAsString( diff --git a/zio-json/jvm/src/jmh/scala/zio/json/SyntheticBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/SyntheticBenchmarks.scala index 26e2ddaad..1f04cd89c 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/SyntheticBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/SyntheticBenchmarks.scala @@ -38,7 +38,7 @@ object Nested { @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(value = 1) class SyntheticBenchmarks { - //@Param(Array("100", "1000")) + // @Param(Array("100", "1000")) var size: Int = 500 var jsonString: String = _ var jsonChars: CharSequence = _ diff --git a/zio-json/jvm/src/jmh/scala/zio/json/internal/SafeNumbersBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/internal/SafeNumbersBenchmarks.scala index 624f8827d..18bf3a5a0 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/internal/SafeNumbersBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/internal/SafeNumbersBenchmarks.scala @@ -10,7 +10,7 @@ import org.openjdk.jmh.annotations._ @Fork(value = 1) class SafeNumbersBenchInt { - //@Param(Array("100", "1000")) + // @Param(Array("100", "1000")) var size: Int = 10000 // invalid input. e.g. out of range longs @@ -69,7 +69,7 @@ class SafeNumbersBenchInt { @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(value = 1) class SafeNumbersBenchFloat { - //@Param(Array("100", "1000")) + // @Param(Array("100", "1000")) var size: Int = 10000 var invalids: Array[String] = _ @@ -135,7 +135,7 @@ class SafeNumbersBenchFloat { @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(value = 1) class SafeNumbersBenchBigDecimal { - //@Param(Array("100", "1000")) + // @Param(Array("100", "1000")) var size: Int = 10000 var invalids: Array[String] = _ diff --git a/zio-json/jvm/src/main/scala/zio/json/JsonDecoderPlatformSpecific.scala b/zio-json/jvm/src/main/scala/zio/json/JsonDecoderPlatformSpecific.scala index 11da0dbfd..7e442f983 100644 --- a/zio-json/jvm/src/main/scala/zio/json/JsonDecoderPlatformSpecific.scala +++ b/zio-json/jvm/src/main/scala/zio/json/JsonDecoderPlatformSpecific.scala @@ -20,12 +20,13 @@ trait JsonDecoderPlatformSpecific[A] { self: JsonDecoder[A] => } /** - * Attempts to decode a stream of bytes using the user supplied Charset into a single value of type `A`, but may fail with - * a human-readable exception if the stream does not encode a value of this type. + * Attempts to decode a stream of bytes using the user supplied Charset into a single value of type `A`, but may fail + * with a human-readable exception if the stream does not encode a value of this type. * * Note: This method may not consume the full string. * - * @see [[decodeJsonStream]] For a `Char` stream variant + * @see + * [[decodeJsonStream]] For a `Char` stream variant */ final def decodeJsonStreamInput[R]( stream: ZStream[R, Throwable, Byte], @@ -41,12 +42,13 @@ trait JsonDecoderPlatformSpecific[A] { self: JsonDecoder[A] => } /** - * Attempts to decode a stream of characters into a single value of type `A`, but may fail with - * a human-readable exception if the stream does not encode a value of this type. + * Attempts to decode a stream of characters into a single value of type `A`, but may fail with a human-readable + * exception if the stream does not encode a value of this type. * * Note: This method may not consume the full string. * - * @see also [[decodeJsonStreamInput]] + * @see + * also [[decodeJsonStreamInput]] */ final def decodeJsonStream[R](stream: ZStream[R, Throwable, Char]): ZIO[R, Throwable, A] = ZIO.scoped[R](stream.toReader.flatMap(readAll)) diff --git a/zio-json/jvm/src/test/scala/zio/json/TestUtils.scala b/zio-json/jvm/src/test/scala/zio/json/TestUtils.scala index e96cfdae7..498fc9cc2 100644 --- a/zio-json/jvm/src/test/scala/zio/json/TestUtils.scala +++ b/zio-json/jvm/src/test/scala/zio/json/TestUtils.scala @@ -15,9 +15,9 @@ object TestUtils { def getResourceAsString(res: String): String = { val is = getClass.getClassLoader.getResourceAsStream(res) try { - val baos = new java.io.ByteArrayOutputStream() - val data = Array.ofDim[Byte](2048) - var len: Int = 0 + val baos = new java.io.ByteArrayOutputStream() + val data = Array.ofDim[Byte](2048) + var len: Int = 0 def read(): Int = { len = is.read(data); len } while (read() != -1) baos.write(data, 0, len) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 73de220c9..65681d0ac 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -10,8 +10,7 @@ import scala.annotation._ import scala.language.experimental.macros /** - * If used on a case class field, determines the name of the JSON field. - * Defaults to the case class field name. + * If used on a case class field, determines the name of the JSON field. Defaults to the case class field name. */ final case class jsonField(name: String) extends Annotation @@ -28,17 +27,14 @@ final class jsonExplicitNull extends Annotation final case class jsonExplicitEmptyCollection(enabled: Boolean = true) extends Annotation /** - * If used on a sealed class, will determine the name of the field for - * disambiguating classes. + * If used on a sealed class, will determine the name of the field for disambiguating classes. * - * The default is to not use a typehint field and instead - * have an object with a single key that is the class name. + * The default is to not use a typehint field and instead have an object with a single key that is the class name. * - * Note that using a discriminator is less performant, uses more memory, and may - * be prone to DOS attacks that are impossible with the default encoding. In - * addition, there is slightly less type safety when using custom product - * encoders (which must write an unenforced object type). Only use this option - * if you must model an externally defined schema. + * Note that using a discriminator is less performant, uses more memory, and may be prone to DOS attacks that are + * impossible with the default encoding. In addition, there is slightly less type safety when using custom product + * encoders (which must write an unenforced object type). Only use this option if you must model an externally defined + * schema. */ final case class jsonDiscriminator(name: String) extends Annotation // TODO a strategy where the constructor is inferred from the field names, only @@ -83,16 +79,14 @@ object ziojson_03 { } /** - * If used on a case class, determines the strategy of member names - * transformation during serialization and deserialization. Four common - * strategies are provided above and a custom one to support specific use cases. + * If used on a case class, determines the strategy of member names transformation during serialization and + * deserialization. Four common strategies are provided above and a custom one to support specific use cases. */ final case class jsonMemberNames(format: JsonMemberFormat) extends Annotation private[json] object jsonMemberNames { /** - * ~~Stolen~~ Borrowed from jsoniter-scala by Andriy Plokhotnyuk - * (he even granted permission for this, imagine that!) + * ~~Stolen~~ Borrowed from jsoniter-scala by Andriy Plokhotnyuk (he even granted permission for this, imagine that!) */ import java.lang.Character._ @@ -175,27 +169,26 @@ private[json] object jsonMemberNames { } /** - * If used on a case class will determine the type hint value for disambiguating - * sealed traits. Defaults to the short type name. + * If used on a case class will determine the type hint value for disambiguating sealed traits. Defaults to the short + * type name. */ final case class jsonHint(name: String) extends Annotation /** - * If used on a sealed class will determine the strategy of type hint value transformation for disambiguating - * classes during serialization and deserialization. Same strategies are provided as for [[jsonMemberNames]]. + * If used on a sealed class will determine the strategy of type hint value transformation for disambiguating classes + * during serialization and deserialization. Same strategies are provided as for [[jsonMemberNames]]. */ final case class jsonHintNames(format: JsonMemberFormat) extends Annotation /** - * If used on a case class, will exit early if any fields are in the JSON that - * do not correspond to field names in the case class. + * If used on a case class, will exit early if any fields are in the JSON that do not correspond to field names in the + * case class. * - * This adds extra protections against a DOS attacks but means that changes in - * the schema will result in a hard error rather than silently ignoring those - * fields. + * This adds extra protections against a DOS attacks but means that changes in the schema will result in a hard error + * rather than silently ignoring those fields. * - * Cannot be combined with `@jsonDiscriminator` since it is considered an extra - * field from the perspective of the case class. + * Cannot be combined with `@jsonDiscriminator` since it is considered an extra field from the perspective of the case + * class. */ final class jsonNoExtraFields extends Annotation diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala index 30bc02f1e..a70233ec5 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala @@ -4,17 +4,16 @@ import scala.compiletime.* import scala.compiletime.ops.any.IsConst private[json] trait JsonDecoderVersionSpecific { - inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonDecoder[A] = DeriveJsonDecoder.gen[A] + inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonDecoder[A] = + DeriveJsonDecoder.gen[A] } trait DecoderLowPriorityVersionSpecific { inline given unionOfStringEnumeration[T](using IsUnionOf[String, T]): JsonDecoder[T] = val values = UnionDerivation.constValueUnionTuple[String, T] - JsonDecoder.string.mapOrFail( - { - case raw if values.toList.contains(raw) => Right(raw.asInstanceOf[T]) - case _ => Left("expected one of: " + values.toList.mkString(", ")) - } - ) + JsonDecoder.string.mapOrFail { + case raw if values.toList.contains(raw) => Right(raw.asInstanceOf[T]) + case _ => Left("expected one of: " + values.toList.mkString(", ")) + } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala index f0d14cf25..82932a7cf 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala @@ -3,7 +3,8 @@ package zio.json import scala.compiletime.ops.any.IsConst private[json] trait JsonEncoderVersionSpecific { - inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonEncoder[A] = DeriveJsonEncoder.gen[A] + inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonEncoder[A] = + DeriveJsonEncoder.gen[A] } private[json] trait EncoderLowPriorityVersionSpecific { diff --git a/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala index c260f472c..e733473eb 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala @@ -15,7 +15,7 @@ private[json] object IsUnionOf: private def deriveImpl[T, A](using quotes: Quotes, t: Type[T], a: Type[A]): Expr[IsUnionOf[T, A]] = import quotes.reflect.* - val tpe: TypeRepr = TypeRepr.of[A] + val tpe: TypeRepr = TypeRepr.of[A] val bound: TypeRepr = TypeRepr.of[T] def validateTypes(tpe: TypeRepr): Unit = diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala index 32e3c53ec..25d368f73 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala @@ -20,20 +20,17 @@ import zio.{ Chunk, NonEmptyChunk } import scala.collection.immutable /** - * A `JsonCodec[A]` instance has the ability to encode values of type `A` into JSON, together with - * the ability to decode such JSON into values of type `A`. + * A `JsonCodec[A]` instance has the ability to encode values of type `A` into JSON, together with the ability to decode + * such JSON into values of type `A`. * - * Instances of this trait should satisfy round-tripping laws: that is, for every value, instances - * must be able to successfully encode the value into JSON, and then successfully decode the same - * value from such JSON. + * Instances of this trait should satisfy round-tripping laws: that is, for every value, instances must be able to + * successfully encode the value into JSON, and then successfully decode the same value from such JSON. * * For more information, see [[JsonDecoder]] and [[JsonEncoder]]. * - * {{ - * val intCodec: JsonCodec[Int] = JsonCodec[Int] + * {{ val intCodec: JsonCodec[Int] = JsonCodec[Int] * - * intCodec.encodeJson(intCodec.encodeJson(42)) == Right(42) - * }} + * intCodec.encodeJson(intCodec.encodeJson(42)) == Right(42) }} */ final case class JsonCodec[A](encoder: JsonEncoder[A], decoder: JsonDecoder[A]) { self => diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala index f6b31f688..2619f54bc 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala @@ -6,10 +6,14 @@ import zio.json.JsonCodecConfiguration.SumTypeHandling.WrapperWithClassNameField /** * Implicit codec derivation configuration. * - * @param sumTypeHandling see [[jsonDiscriminator]] - * @param fieldNameMapping see [[jsonMemberNames]] - * @param allowExtraFields see [[jsonNoExtraFields]] - * @param sumTypeMapping see [[jsonHintNames]] + * @param sumTypeHandling + * see [[jsonDiscriminator]] + * @param fieldNameMapping + * see [[jsonMemberNames]] + * @param allowExtraFields + * see [[jsonNoExtraFields]] + * @param sumTypeMapping + * see [[jsonHintNames]] */ final case class JsonCodecConfiguration( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, @@ -37,18 +41,15 @@ object JsonCodecConfiguration { } /** - * For sealed classes, will determine the name of the field for - * disambiguating classes. + * For sealed classes, will determine the name of the field for disambiguating classes. * - * The default is to not use a typehint field and instead - * have an object with a single key that is the class name. + * The default is to not use a typehint field and instead have an object with a single key that is the class name. * See [[WrapperWithClassNameField]]. * - * Note that using a discriminator is less performant, uses more memory, and may - * be prone to DOS attacks that are impossible with the default encoding. In - * addition, there is slightly less type safety when using custom product - * encoders (which must write an unenforced object type). Only use this option - * if you must model an externally defined schema. + * Note that using a discriminator is less performant, uses more memory, and may be prone to DOS attacks that are + * impossible with the default encoding. In addition, there is slightly less type safety when using custom product + * encoders (which must write an unenforced object type). Only use this option if you must model an externally + * defined schema. */ final case class DiscriminatorField(name: String) extends SumTypeHandling { override def discriminatorField: Option[String] = Some(name) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index c3c68ea21..4972fc834 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -28,8 +28,8 @@ import scala.collection.{ immutable, mutable } import scala.util.control.NoStackTrace /** - * A `JsonDecoder[A]` instance has the ability to decode JSON to values of type `A`, potentially - * failing with an error if the JSON content does not encode a value of the given type. + * A `JsonDecoder[A]` instance has the ability to decode JSON to values of type `A`, potentially failing with an error + * if the JSON content does not encode a value of the given type. */ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { self => @@ -60,8 +60,8 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { final def <*[B](that: => JsonDecoder[B]): JsonDecoder[A] = self.zipLeft(that) /** - * Attempts to decode a value of type `A` from the specified `CharSequence`, but may fail with - * a human-readable error message if the provided text does not encode a value of this type. + * Attempts to decode a value of type `A` from the specified `CharSequence`, but may fail with a human-readable error + * message if the provided text does not encode a value of this type. * * Note: This method may not entirely consume the specified character sequence. */ @@ -79,13 +79,11 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { final def widen[B >: A]: JsonDecoder[B] = self.asInstanceOf[JsonDecoder[B]] /** - * Returns a new codec that combines this codec and the specified codec using fallback semantics: - * such that if this codec fails, the specified codec will be tried instead. - * This method may be unsafe from a security perspective: it can use more memory than hand coded - * alternative and so lead to DOS. + * Returns a new codec that combines this codec and the specified codec using fallback semantics: such that if this + * codec fails, the specified codec will be tried instead. This method may be unsafe from a security perspective: it + * can use more memory than hand coded alternative and so lead to DOS. * - * For example, in the case of an alternative between `Int` and `Boolean`, a hand coded - * alternative would look like: + * For example, in the case of an alternative between `Int` and `Boolean`, a hand coded alternative would look like: * * ``` * val decoder: JsonDecoder[AnyVal] = JsonDecoder.peekChar[AnyVal] { @@ -127,8 +125,8 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { } /** - * Returns a new codec that combines this codec and the specified codec using fallback semantics: - * such that if this codec fails, the specified codec will be tried instead. + * Returns a new codec that combines this codec and the specified codec using fallback semantics: such that if this + * codec fails, the specified codec will be tried instead. */ final def orElseEither[B](that: => JsonDecoder[B]): JsonDecoder[Either[A, B]] = self.map(Left(_)).orElse(that.map(Right(_))) @@ -151,8 +149,8 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { } /** - * Returns a new codec whose decoded values will be mapped by the specified function, which may - * itself decide to fail with some type of error. + * Returns a new codec whose decoded values will be mapped by the specified function, which may itself decide to fail + * with some type of error. */ final def mapOrFail[B](f: A => Either[String, B]): JsonDecoder[B] = new JsonDecoder[B] { @@ -180,8 +178,8 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { } /** - * Returns a new codec that combines this codec and the specified codec into a single codec that - * decodes a tuple of the values decoded by the respective codecs. + * Returns a new codec that combines this codec and the specified codec into a single codec that decodes a tuple of + * the values decoded by the respective codecs. */ final def zip[B](that: => JsonDecoder[B]): JsonDecoder[(A, B)] = JsonDecoder.tuple2(this, that) @@ -205,8 +203,8 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { throw JsonDecoder.UnsafeJson(JsonError.Message("missing") :: trace) /** - * Low-level, unsafe method to decode a value or throw an exception. This method should not be - * called in application code, although it can be implemented for user-defined data structures. + * Low-level, unsafe method to decode a value or throw an exception. This method should not be called in application + * code, although it can be implemented for user-defined data structures. */ def unsafeDecode(trace: List[JsonError], in: RetractReader): A @@ -216,8 +214,8 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { /** * Decode a value from an already parsed Json AST. * - * The default implementation encodes the Json to a byte stream and uses decode to parse that. - * Override to provide a more performant implementation. + * The default implementation encodes the Json to a byte stream and uses decode to parse that. Override to provide a + * more performant implementation. */ final def fromJsonAST(json: Json): Either[String, A] = try Right(unsafeFromJsonAST(Nil, json)) @@ -236,10 +234,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with def apply[A](implicit a: JsonDecoder[A]): JsonDecoder[A] = a /** - * Design note: we could require the position in the stream here to improve - * debugging messages. But the cost would be that the RetractReader would need - * to keep track and any wrappers would need to preserve the position. It may - * still be desirable to do this but at the moment it is not necessary. + * Design note: we could require the position in the stream here to improve debugging messages. But the cost would be + * that the RetractReader would need to keep track and any wrappers would need to preserve the position. It may still + * be desirable to do this but at the moment it is not necessary. */ final case class UnsafeJson(trace: List[JsonError]) extends Exception("If you see this, a developer made a mistake using JsonDecoder") diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 66d179576..86b65e0d8 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -29,8 +29,8 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { self => /** - * Returns a new encoder, with a new input type, which can be transformed to the old input type - * by the specified user-defined function. + * Returns a new encoder, with a new input type, which can be transformed to the old input type by the specified + * user-defined function. */ final def contramap[B](f: B => A): JsonEncoder[B] = new JsonEncoder[B] { @@ -46,25 +46,22 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { } /** - * Returns a new encoder that can accepts an `Either[A, B]` to either, and uses either this - * encoder or the specified encoder to encode the two different types of values. + * Returns a new encoder that can accepts an `Either[A, B]` to either, and uses either this encoder or the specified + * encoder to encode the two different types of values. */ final def either[B](that: => JsonEncoder[B]): JsonEncoder[Either[A, B]] = JsonEncoder.either[A, B](self, that) /** - * Returns a new encoder that can accepts an `Either[A, B]` to either, and uses either this - * encoder or the specified encoder to encode the two different types of values. - * The difference with the classic `either` encoder is that the resulting JSON has no field - * `Left` or `Right`. - * What should be: `{"Right": "John Doe"}` is encoded as `"John Doe"` + * Returns a new encoder that can accepts an `Either[A, B]` to either, and uses either this encoder or the specified + * encoder to encode the two different types of values. The difference with the classic `either` encoder is that the + * resulting JSON has no field `Left` or `Right`. What should be: `{"Right": "John Doe"}` is encoded as `"John Doe"` */ final def orElseEither[B](that: => JsonEncoder[B]): JsonEncoder[Either[A, B]] = JsonEncoder.orElseEither[A, B](self, that) /** - * Returns a new encoder with a new input type, which can be transformed to either the input - * type of this encoder, or the input type of the specified encoder, using the user-defined - * transformation function. + * Returns a new encoder with a new input type, which can be transformed to either the input type of this encoder, or + * the input type of the specified encoder, using the user-defined transformation function. */ final def eitherWith[B, C](that: => JsonEncoder[B])(f: C => Either[A, B]): JsonEncoder[C] = self.either(that).contramap(f) @@ -79,14 +76,12 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { } /** - * This default may be overridden when this value may be missing within a JSON object and still - * be encoded. + * This default may be overridden when this value may be missing within a JSON object and still be encoded. */ def isNothing(a: A): Boolean = false /** - * This default may be overridden when this value may be empty within a JSON object and still - * be encoded. + * This default may be overridden when this value may be empty within a JSON object and still be encoded. */ def isEmpty(a: A): Boolean = false @@ -100,22 +95,20 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { /** * Converts a value to a Json AST * - * The default implementation encodes the value to a Json byte stream and - * uses decode to parse that back to an AST. Override to provide a more performant - * implementation. + * The default implementation encodes the value to a Json byte stream and uses decode to parse that back to an AST. + * Override to provide a more performant implementation. */ def toJsonAST(a: A): Either[String, Json] = Json.decoder.decodeJson(encodeJson(a, None)) /** - * Returns a new encoder that is capable of encoding a tuple containing the values of this - * encoder and the specified encoder. + * Returns a new encoder that is capable of encoding a tuple containing the values of this encoder and the specified + * encoder. */ final def zip[B](that: => JsonEncoder[B]): JsonEncoder[(A, B)] = JsonEncoder.tuple2(self, that) /** - * Returns a new encoder that is capable of encoding a user-defined value, which is create from - * a tuple of the values of this encoder and the specified encoder, from the specified user- - * defined function. + * Returns a new encoder that is capable of encoding a user-defined value, which is create from a tuple of the values + * of this encoder and the specified encoder, from the specified user- defined function. */ final def zipWith[B, C](that: => JsonEncoder[B])(f: C => (A, B)): JsonEncoder[C] = self.zip(that).contramap(f) } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonError.scala b/zio-json/shared/src/main/scala/zio/json/JsonError.scala index b9525ad06..f82ded949 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonError.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonError.scala @@ -16,8 +16,8 @@ package zio.json /** - * A `JsonError` value describes the ways in which decoding could fail. This structure is used - * to facilitate human-readable error messages during decoding failures. + * A `JsonError` value describes the ways in which decoding could fail. This structure is used to facilitate + * human-readable error messages during decoding failures. */ sealed abstract class JsonError diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index 694d95ae1..a1f44f21a 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -24,19 +24,15 @@ import zio.json.internal._ import scala.annotation._ /** - * This AST of JSON is made available so that arbitrary JSON may be included as - * part of a business object, it is not used as an intermediate representation, - * unlike most other JSON libraries. It is not advised to `.map` or `.mapOrFail` + * This AST of JSON is made available so that arbitrary JSON may be included as part of a business object, it is not + * used as an intermediate representation, unlike most other JSON libraries. It is not advised to `.map` or `.mapOrFail` * from these decoders, since a higher performance decoder is often available. * - * Beware of the potential for DOS attacks, since an attacker can provide much - * more data than is perhaps needed. + * Beware of the potential for DOS attacks, since an attacker can provide much more data than is perhaps needed. * - * Also beware of converting `Num` (a `BigDecimal`) into any other kind of - * number, since many of the stdlib functions are non-total or are known DOS - * vectors (e.g. calling `.toBigInteger` on a "1e214748364" will consume an - * excessive amount of heap memory). - * JsonValue / Json / JValue + * Also beware of converting `Num` (a `BigDecimal`) into any other kind of number, since many of the stdlib functions + * are non-total or are known DOS vectors (e.g. calling `.toBigInteger` on a "1e214748364" will consume an excessive + * amount of heap memory). JsonValue / Json / JValue */ sealed abstract class Json { self => final def as[A](implicit decoder: JsonDecoder[A]): Either[String, A] = decoder.fromJsonAST(self) @@ -70,8 +66,10 @@ sealed abstract class Json { self => /** * Deletes json node specified by given cursor - * @param cursor Cursor which specifies node to delete - * @return Json without specified node if node specified by cursor exists, error otherwise + * @param cursor + * Cursor which specifies node to delete + * @return + * Json without specified node if node specified by cursor exists, error otherwise */ final def delete(cursor: JsonCursor[_, _]): Either[String, Json] = { val c = cursor.asInstanceOf[JsonCursor[_, Json]] @@ -189,10 +187,11 @@ sealed abstract class Json { self => } /** - * Intersects JSON values. If both values are `Obj` or `Arr` method returns intersections of its fields/elements, otherwise - * it returns error + * Intersects JSON values. If both values are `Obj` or `Arr` method returns intersections of its fields/elements, + * otherwise it returns error * @param that - * @return Intersected json if type are compatible, error otherwise + * @return + * Intersected json if type are compatible, error otherwise */ final def intersect(that: Json): Either[String, Json] = (self, that) match { @@ -204,12 +203,12 @@ sealed abstract class Json { self => } /** - * - merging objects results in a new objects with all pairs of both sides, with the right hand - * side being used on key conflicts + * - merging objects results in a new objects with all pairs of both sides, with the right hand side being used on + * key conflicts * - * - merging arrays results in all of the individual elements being merged + * - merging arrays results in all of the individual elements being merged * - * - scalar values will be replaced by the right hand side + * - scalar values will be replaced by the right hand side */ final def merge(that: Json): Json = (self, that) match { @@ -221,10 +220,14 @@ sealed abstract class Json { self => /** * Relocates Json node from location specified by `from` cursor to location specified by `to` cursor. * - * @param from Cursor which specifies node to relocate - * @return Json without specified node if node specified by cursor exists, error otherwise - * @param to Cursor which specifies location where to relocate node - * @return Json with relocated node if node specified by cursors exist, error otherwise + * @param from + * Cursor which specifies node to relocate + * @return + * Json without specified node if node specified by cursor exists, error otherwise + * @param to + * Cursor which specifies location where to relocate node + * @return + * Json with relocated node if node specified by cursors exist, error otherwise */ final def relocate(from: JsonCursor[_, _], to: JsonCursor[_, _]): Either[String, Json] = { val f = from.asInstanceOf[JsonCursor[_, Json]] @@ -234,10 +237,14 @@ sealed abstract class Json { self => /** * Transforms json node specified by given cursor - * @param cursor Cursor which specifies node to transform - * @param f Function used to transform node - * @tparam A refined node type - * @return Json with transformed node if node specified by cursor exists, error otherwise + * @param cursor + * Cursor which specifies node to transform + * @param f + * Function used to transform node + * @tparam A + * refined node type + * @return + * Json with transformed node if node specified by cursor exists, error otherwise */ final def transformAt[A <: Json](cursor: JsonCursor[_, A])(f: A => Json): Either[String, Json] = transformOrDelete(cursor, delete = false)(x => Right(f(x))) diff --git a/zio-json/shared/src/main/scala/zio/json/codegen/Generator.scala b/zio-json/shared/src/main/scala/zio/json/codegen/Generator.scala index 523dd35b1..70e4d281f 100644 --- a/zio-json/shared/src/main/scala/zio/json/codegen/Generator.scala +++ b/zio-json/shared/src/main/scala/zio/json/codegen/Generator.scala @@ -16,8 +16,7 @@ import scala.util.Try object Generator { /** - * Renders the JSON string as a series of Scala case classes derived from the - * structure of the JSON. + * Renders the JSON string as a series of Scala case classes derived from the structure of the JSON. * * For example, the following JSON: * diff --git a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala index c869f113e..979cb838f 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala @@ -21,28 +21,22 @@ import scala.util.control.NoStackTrace /** * Total, fast, number parsing. * - * The Java and Scala standard libraries throw exceptions when we attempt to - * parse an invalid number. Unfortunately, exceptions are very expensive, and - * untrusted data can be maliciously constructed to DOS a server. + * The Java and Scala standard libraries throw exceptions when we attempt to parse an invalid number. Unfortunately, + * exceptions are very expensive, and untrusted data can be maliciously constructed to DOS a server. * - * This suite of functions mitigates against such attacks by building up the - * numbers one character at a time, which has been shown through extensive - * benchmarking to be orders of magnitude faster than exception-throwing stdlib - * parsers, for valid and invalid inputs. This approach, proposed by alexknvl, - * was also benchmarked against regexp-based pre-validation. + * This suite of functions mitigates against such attacks by building up the numbers one character at a time, which has + * been shown through extensive benchmarking to be orders of magnitude faster than exception-throwing stdlib parsers, + * for valid and invalid inputs. This approach, proposed by alexknvl, was also benchmarked against regexp-based + * pre-validation. * - * Note that although the behaviour is identical to the Java stdlib when given - * the canonical form of a primitive (i.e. the .toString) of a number there may - * be differences in behaviour for non-canonical forms. e.g. the Java stdlib - * may reject "1.0" when parsed as an `BigInteger` but we may parse it as a - * `1`, although "1.1" would be rejected. Parsing of `BigDecimal` preserves the - * trailing zeros on the right but not on the left, e.g. "000.00001000" will be - * "1.000e-5", which is useful in cases where the trailing zeros denote - * measurement accuracy. + * Note that although the behaviour is identical to the Java stdlib when given the canonical form of a primitive (i.e. + * the .toString) of a number there may be differences in behaviour for non-canonical forms. e.g. the Java stdlib may + * reject "1.0" when parsed as an `BigInteger` but we may parse it as a `1`, although "1.1" would be rejected. Parsing + * of `BigDecimal` preserves the trailing zeros on the right but not on the left, e.g. "000.00001000" will be + * "1.000e-5", which is useful in cases where the trailing zeros denote measurement accuracy. * - * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit - * limit on the size of the significand, to avoid OOM style attacks, which is - * 128 bits by default. + * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit limit on the size of the significand, to + * avoid OOM style attacks, which is 128 bits by default. * * Results are contained in a specialisation of Option that avoids boxing. */ @@ -186,7 +180,8 @@ object SafeNumbers { s.insert(dotOff, '.') } else s.append(dv).append('.').append('0') } - }.toString + s.toString + } } def toString(x: Float): String = { @@ -280,7 +275,8 @@ object SafeNumbers { s.insert(dotOff, '.') } else s.append(dv).append('.').append('0') } - }.toString + s.toString + } } private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { diff --git a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala index 6d81eb43a..95a715b8f 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala @@ -75,9 +75,8 @@ private[zio] final class RewindTwice /** * A Reader that can retract and replay the last char that it read. * - * This is essential when parsing contents that do not have a terminator - * character, e.g. numbers, whilst preserving the non-significant character for - * further processing. + * This is essential when parsing contents that do not have a terminator character, e.g. numbers, whilst preserving the + * non-significant character for further processing. */ sealed trait RetractReader extends OneCharReader { @@ -150,13 +149,11 @@ final class WithRetractReader(in: java.io.Reader) extends RetractReader with Aut } /** - * Records the contents of an underlying Reader and allows rewinding back to - * the beginning once. If rewound and reading continues past the - * recording, the recording no longer continues. + * Records the contents of an underlying Reader and allows rewinding back to the beginning once. If rewound and reading + * continues past the recording, the recording no longer continues. * - * To avoid feature interaction edge cases, `retract` is not allowed as the - * first action nor is `retract` allowed to happen immediately before or after - * a `rewind`. + * To avoid feature interaction edge cases, `retract` is not allowed as the first action nor is `retract` allowed to + * happen immediately before or after a `rewind`. */ private[zio] sealed trait RecordingReader extends RetractReader { def rewind(): Unit diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala index 9fa4365b7..4f58364c6 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala @@ -57,9 +57,9 @@ private[json] object serializers { val epochDay = (if (epochSecond >= 0) epochSecond else epochSecond - 86399) / 86400 // 86400 == seconds per day - val secsOfDay = (epochSecond - epochDay * 86400).toInt - var marchZeroDay = epochDay + 719468 // 719468 == 719528 - 60 == days 0000 to 1970 - days 1st Jan to 1st Mar - var adjustYear = 0 + val secsOfDay = (epochSecond - epochDay * 86400).toInt + var marchZeroDay = epochDay + 719468 // 719468 == 719528 - 60 == days 0000 to 1970 - days 1st Jan to 1st Mar + var adjustYear = 0 if (marchZeroDay < 0) { // adjust negative years to positive for calculation val adjust400YearCycles = to400YearCycle(marchZeroDay + 1) - 1 adjustYear = adjust400YearCycles * 400 diff --git a/zio-json/shared/src/main/scala/zio/json/package.scala b/zio-json/shared/src/main/scala/zio/json/package.scala index b9747b164..7ec99fd4a 100644 --- a/zio-json/shared/src/main/scala/zio/json/package.scala +++ b/zio-json/shared/src/main/scala/zio/json/package.scala @@ -32,11 +32,9 @@ package object json extends JsonPackagePlatformSpecific { /** * Attempts to decode the raw JSON string as an `A`. * - * On failure a human readable message is returned using a jq friendly - * format. For example the error - * `.rows[0].elements[0].distance.value(missing)"` tells us the location of a - * missing field named "value". We can use part of the error message in the - * `jq` command line tool for further inspection, e.g. + * On failure a human readable message is returned using a jq friendly format. For example the error + * `.rows[0].elements[0].distance.value(missing)"` tells us the location of a missing field named "value". We can + * use part of the error message in the `jq` command line tool for further inspection, e.g. * * {{{jq '.rows[0].elements[0].distance' input.json}}} */ diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala index 84d1f2313..433042bd5 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala @@ -32,6 +32,6 @@ object DerivedCodecSpec extends ZIOSpecDefault { case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonCodec assertTrue(Foo("A", Some("A")).toJson.fromJson[Foo] == Right(Foo("A", Some("A")))) - }, + } ) } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala index b99dd7cd2..d599c5b72 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala @@ -22,7 +22,7 @@ object DerivedDecoderSpec extends ZIOSpecDefault { case Qux val result = "\"Qux\"".fromJson[Foo] - + assertTrue(result == Right(Foo.Qux)) }, test("Derives for a sum sealed trait Enumeration type") { @@ -33,7 +33,7 @@ object DerivedDecoderSpec extends ZIOSpecDefault { case object Qux extends Foo val result = "\"Qux\"".fromJson[Foo] - + assertTrue(result == Right(Foo.Qux)) }, test("Derives for a sum sealed trait Enumeration type with discriminator") { @@ -45,7 +45,7 @@ object DerivedDecoderSpec extends ZIOSpecDefault { case object Qux extends Foo val result = """{"$type":"Qux"}""".fromJson[Foo] - + assertTrue(result == Right(Foo.Qux)) }, test("Derives for a sum ADT type") { From 5a37352a47976a4a597d44577f93c91037b8495d Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 5 Jan 2025 15:06:21 +0100 Subject: [PATCH 062/311] Update auxlib, clib, javalib, nativelib, ... to 0.5.6 (#1191) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 20f449927..2ffed054f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.5") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") From b082da5f25a9123311ab28163f86ed72fd099040 Mon Sep 17 00:00:00 2001 From: kyri-petrou <67301607+kyri-petrou@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:40:30 +0200 Subject: [PATCH 063/311] Update ZIO version (#1205) --- .github/workflows/ci.yml | 7 +++++-- build.sbt | 12 +++++------- project/BuildHelper.scala | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 880ed5ce6..d57b98bd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: uses: actions/setup-java@v4.2.1 with: distribution: temurin - java-version: 8 + java-version: 11 check-latest: true - name: Cache scala dependencies uses: coursier/cache-action@v6 @@ -86,6 +86,9 @@ jobs: uses: actions/checkout@v4.1.2 with: fetch-depth: 0 + - name: Install Boehm GC + if: ${{ startsWith(matrix.platform, 'Native') }} + run: sudo apt-get update && sudo apt-get install -y libgc-dev - name: Setup Java uses: actions/setup-java@v4.2.1 with: @@ -124,7 +127,7 @@ jobs: uses: actions/setup-java@v4.2.1 with: distribution: temurin - java-version: 8 + java-version: 11 check-latest: true - name: Release run: sbt ci-release diff --git a/build.sbt b/build.sbt index eed3b12a6..5b5bb35c5 100644 --- a/build.sbt +++ b/build.sbt @@ -55,7 +55,7 @@ addCommandAlias( "zioJsonNative/test; zioJsonInteropScalaz7xNative/test" ) -val zioVersion = "2.1.7" +val zioVersion = "2.1.14" lazy val zioJsonRoot = project .in(file(".")) @@ -250,12 +250,11 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) } } ) - .nativeSettings(Test / fork := false) + .nativeSettings(nativeSettings) .nativeSettings( libraryDependencies ++= Seq( "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion - ), - nativeConfig ~= { _.withMultithreading(false) } + ) ) .enablePlugins(BuildInfoPlugin) @@ -314,10 +313,9 @@ lazy val zioJsonMacros = crossProject(JSPlatform, JVMPlatform, NativePlatform) "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test" ), - testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), - nativeConfig ~= { _.withMultithreading(false) } + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) - .nativeSettings(Test / fork := false) + .nativeSettings(nativeSettings) lazy val zioJsonMacrosJVM = zioJsonMacros.jvm.dependsOn(zioJsonJVM) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index fddccb722..bd5f1b296 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -6,6 +6,8 @@ import sbtbuildinfo.* import sbtbuildinfo.BuildInfoKeys.* import sbtcrossproject.CrossPlugin.autoImport.* +import scala.scalanative.sbtplugin.ScalaNativePlugin.autoImport.* + object BuildHelper { private val versions: Map[String, String] = { import org.snakeyaml.engine.v2.api.{ Load, LoadSettings } @@ -275,9 +277,16 @@ object BuildHelper { ) def nativeSettings = Seq( - Test / skip := true, - doc / skip := true, - Compile / doc / sources := Seq.empty + nativeConfig ~= { cfg => + import scala.scalanative.build.{ GC, Mode } + + val os = System.getProperty("os.name").toLowerCase + // For some unknown reason, we can't run the test suites in debug mode on MacOS + if (os.contains("mac")) cfg.withMode(Mode.releaseFast) + else cfg.withGC(GC.boehm) // See https://github.com/scala-native/scala-native/issues/4032 + }, + scalacOptions += "-P:scalanative:genStaticForwardersForNonTopLevelObjects", + Test / fork := false ) val scalaReflectTestSettings: List[Setting[_]] = List( From c45263656ebf39d4ca659bf61afa7d03b4ead6e8 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Tue, 7 Jan 2025 07:36:53 +0100 Subject: [PATCH 064/311] Update workflow --- .github/workflows/site.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 8b33f0ba2..6e1f40ea9 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -14,7 +14,7 @@ name: Website jobs: build: name: Build and Test - runs-on: ubuntu-22.04 + runs-on: ubuntu-20.04 if: ${{ github.event_name == 'pull_request' }} steps: - name: Git Checkout @@ -33,7 +33,7 @@ jobs: run: sbt docs/clean; sbt docs/buildWebsite publish-docs: name: Publish Docs - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 if: ${{ ((github.event_name == 'release') && (github.event.action == 'published')) || (github.event_name == 'workflow_dispatch') }} steps: - name: Git Checkout @@ -57,7 +57,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} generate-readme: name: Generate README - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 if: ${{ (github.event_name == 'push') || ((github.event_name == 'release') && (github.event.action == 'published')) }} steps: - name: Git Checkout From f23dc38ddd8ec02fd7407688e6a1028d50a7fde0 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 12 Jan 2025 00:10:52 +0100 Subject: [PATCH 065/311] More efficient parsing of optional values (#1210) --- .../src/main/scala-2.x/zio/json/macros.scala | 87 +++++++----------- .../src/main/scala-3/zio/json/macros.scala | 90 ++++++------------- .../src/main/scala/zio/json/JsonDecoder.scala | 27 +++--- 3 files changed, 73 insertions(+), 131 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 65681d0ac..9d6351c6a 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -270,6 +270,9 @@ object DeriveJsonDecoder { lazy val namesMap: Map[String, Int] = (names.zipWithIndex ++ aliases).toMap + private[this] def error(message: String, trace: List[JsonError]): Nothing = + throw UnsafeJson(JsonError.Message(message) :: trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') @@ -280,36 +283,31 @@ object DeriveJsonDecoder { // of noting that things have been initialised), which can be called // to instantiate the case class. Would also require JsonDecoder to be // specialised. - val ps: Array[Any] = Array.ofDim(len) - + val ps = new Array[Any](len) if (Lexer.firstField(trace, in)) do { - var trace_ = trace - val field = Lexer.field(trace, in, matrix) + val field = Lexer.field(trace, in, matrix) if (field != -1) { - trace_ = spans(field) :: trace - if (ps(field) != null) - throw UnsafeJson(JsonError.Message("duplicate") :: trace) - if (defaults(field).isDefined) { - val opt = JsonDecoder.option(tcs(field)).unsafeDecode(trace_, in) - ps(field) = opt.getOrElse(defaults(field).get) - } else - ps(field) = tcs(field).unsafeDecode(trace_, in) - } else if (no_extra) { - throw UnsafeJson( - JsonError.Message(s"invalid extra field") :: trace - ) - } else - Lexer.skipValue(trace_, in) + if (ps(field) != null) error("duplicate", trace) + val default = defaults(field) + ps(field) = + if ( + (default eq None) || in.nextNonWhitespace() != 'n' && { + in.retract() + true + } + ) tcs(field).unsafeDecode(spans(field) :: trace, in) + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get + else error("expected 'null'", spans(field) :: trace) + } else if (no_extra) error("invalid extra field", trace) + else Lexer.skipValue(trace, in) } while (Lexer.nextField(trace, in)) - var i = 0 while (i < len) { if (ps(i) == null) { - if (defaults(i).isDefined) - ps(i) = defaults(i).get - else - ps(i) = tcs(i).unsafeDecodeMissing(spans(i) :: trace) + ps(i) = + if (defaults(i) ne None) defaults(i).get + else tcs(i).unsafeDecodeMissing(spans(i) :: trace) } i += 1 } @@ -320,51 +318,30 @@ object DeriveJsonDecoder { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case Json.Obj(fields) => - val ps: Array[Any] = Array.ofDim(len) - - if (aliases.nonEmpty) { - val present = fields.map { case (key, _) => namesMap(key) } - if (present.distinct.size != present.size) { - throw UnsafeJson( - JsonError.Message("duplicate") :: trace - ) - } - } - + val ps = new Array[Any](len) for ((key, value) <- fields) { namesMap.get(key) match { case Some(field) => - val trace_ = JsonError.ObjectAccess(key) :: trace - if (defaults(field).isDefined) { - val opt = JsonDecoder.option(tcs(field)).unsafeFromJsonAST(trace_, value) - ps(field) = opt.getOrElse(defaults(field).get) - } else { - ps(field) = tcs(field).unsafeFromJsonAST(trace_, value) - } - case None => - if (no_extra) { - throw UnsafeJson( - JsonError.Message(s"invalid extra field") :: trace - ) + if (ps(field) != null) error("duplicate", trace) + ps(field) = { + if ((value eq Json.Null) && (defaults(field) ne None)) defaults(field).get + else tcs(field).unsafeFromJsonAST(spans(field) :: trace, value) } + case _ => + if (no_extra) error("invalid extra field", trace) } } - var i = 0 while (i < len) { if (ps(i) == null) { - if (defaults(i).isDefined) { - ps(i) = defaults(i).get - } else { - ps(i) = tcs(i).unsafeDecodeMissing(JsonError.ObjectAccess(names(i)) :: trace) - } + ps(i) = + if (defaults(i) ne None) defaults(i).get + else tcs(i).unsafeDecodeMissing(spans(i) :: trace) } i += 1 } - ctx.rawConstruct(new ArraySeq(ps)) - - case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) + case _ => error("Not an object", trace) } } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 672b52cd8..a6b34de64 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -295,100 +295,68 @@ final class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriva lazy val namesMap: Map[String, Int] = (names.zipWithIndex ++ aliases).toMap + private[this] def error(message: String, trace: List[JsonError]): Nothing = + throw UnsafeJson(JsonError.Message(message) :: trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') - - val ps: Array[Any] = Array.ofDim(len) - + val ps = new Array[Any](len) if (Lexer.firstField(trace, in)) while({ - var trace_ = trace val field = Lexer.field(trace, in, matrix) if (field != -1) { - trace_ = spans(field) :: trace - if (ps(field) != null) - throw UnsafeJson(JsonError.Message("duplicate") :: trace) - if (defaults(field).isDefined) { - val opt = JsonDecoder.option(tcs(field)).unsafeDecode(trace_, in) - ps(field) = opt.getOrElse(defaults(field).get) - } else - ps(field) = tcs(field).unsafeDecode(trace_, in) - } else if (no_extra) { - throw UnsafeJson( - JsonError.Message(s"invalid extra field") :: trace - ) - } else - Lexer.skipValue(trace_, in) - + if (ps(field) != null) error("duplicate", trace) + val default = defaults(field) + ps(field) = if ((default eq None) || in.nextNonWhitespace() != 'n' && { + in.retract() + true + }) tcs(field).unsafeDecode(spans(field) :: trace, in) + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get + else error("expected 'null'", spans(field) :: trace) + } else if (no_extra) error("invalid extra field", trace) + else Lexer.skipValue(trace, in) Lexer.nextField(trace, in) }) () - var i = 0 - while (i < len) { if (ps(i) == null) { - if (defaults(i).isDefined) { - ps(i) = defaults(i).get - } else { - ps(i) = tcs(i).unsafeDecodeMissing(spans(i) :: trace) - } + ps(i) = + if (defaults(i) ne None) defaults(i).get + else tcs(i).unsafeDecodeMissing(spans(i) :: trace) } i += 1 } - ctx.rawConstruct(new ArraySeq(ps)) } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = { + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case Json.Obj(fields) => - val ps: Array[Any] = Array.ofDim(len) - - if (aliases.nonEmpty) { - val present = fields.map { case (key, _) => namesMap(key) } - if (present.distinct.size != present.size) { - throw UnsafeJson( - JsonError.Message("duplicate") :: trace - ) - } - } - + val ps = new Array[Any](len) for ((key, value) <- fields) { namesMap.get(key) match { case Some(field) => - val trace_ = JsonError.ObjectAccess(key) :: trace - if (defaults(field).isDefined) { - val opt = JsonDecoder.option(tcs(field)).unsafeFromJsonAST(trace_, value) - ps(field) = opt.getOrElse(defaults(field).get) - } else { - ps(field) = tcs(field).unsafeFromJsonAST(trace_, value) - } - case None => - if (no_extra) { - throw UnsafeJson( - JsonError.Message(s"invalid extra field") :: trace - ) + if (ps(field) != null) error("duplicate", trace) + ps(field) = { + if ((value eq Json.Null) && (defaults(field) ne None)) defaults(field).get + else tcs(field).unsafeFromJsonAST(spans(field) :: trace, value) } + case _ => + if (no_extra) error("invalid extra field", trace) } } - var i = 0 while (i < len) { if (ps(i) == null) { - if (defaults(i).isDefined) { - ps(i) = defaults(i).get - } else { - ps(i) = tcs(i).unsafeDecodeMissing(JsonError.ObjectAccess(names(i)) :: trace) - } + ps(i) = + if (defaults(i) ne None) defaults(i).get + else tcs(i).unsafeDecodeMissing(spans(i) :: trace) } i += 1 } - ctx.rawConstruct(new ArraySeq(ps)) - - case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) + case _ => error("Not an object", trace) } - } } } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 4972fc834..1ec56706d 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -347,26 +347,23 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with // use a newtype wrapper. implicit def option[A](implicit A: JsonDecoder[A]): JsonDecoder[Option[A]] = new JsonDecoder[Option[A]] { self => - private[this] val ull: Array[Char] = "ull".toCharArray - - override def unsafeDecodeMissing(trace: List[JsonError]): Option[A] = - Option.empty + override def unsafeDecodeMissing(trace: List[JsonError]): Option[A] = None def unsafeDecode(trace: List[JsonError], in: RetractReader): Option[A] = - (in.nextNonWhitespace(): @switch) match { - case 'n' => - Lexer.readChars(trace, in, ull, "null") - None - case _ => - in.retract() - Some(A.unsafeDecode(trace, in)) + if (in.nextNonWhitespace() == 'n') { + if (in.readChar() != 'u' || in.readChar() != 'l' || in.readChar() != 'l') error(trace) + None + } else { + in.retract() + new Some(A.unsafeDecode(trace, in)) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Option[A] = - json match { - case Json.Null => None - case _ => Some(A.unsafeFromJsonAST(trace, json)) - } + if (json eq Json.Null) None + else new Some(A.unsafeFromJsonAST(trace, json)) + + private[this] def error(trace: List[JsonError]): Option[A] = + throw UnsafeJson(JsonError.Message("expected 'null'") :: trace) } // supports multiple representations for compatibility with other libraries, From 528e1ac69bdd8b1ed5e26aca41fcb61f10f1029b Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 12 Jan 2025 12:18:50 +0100 Subject: [PATCH 066/311] Fix string matrix (#1211) --- .../zio/json/internal/StringMatrixSpec.scala | 8 +- .../main/scala/zio/json/internal/lexer.scala | 73 +++++++++---------- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/zio-json/jvm/src/test/scala/zio/json/internal/StringMatrixSpec.scala b/zio-json/jvm/src/test/scala/zio/json/internal/StringMatrixSpec.scala index 8ae865a76..1dd5f2992 100644 --- a/zio-json/jvm/src/test/scala/zio/json/internal/StringMatrixSpec.scala +++ b/zio-json/jvm/src/test/scala/zio/json/internal/StringMatrixSpec.scala @@ -7,7 +7,7 @@ import zio.test._ object StringMatrixSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = suite("StringMatrix")( test("basic positive succeeds") { - val names = List("a", "b") + val names = List("\uD83D\uDE00" /* a surrogate pair for the grinning face */, "a", "b") val aliases = List("c" -> 0, "d" -> 1) val asserts = names.map(s => matcher(names, aliases, s).contains(s)) ++ @@ -115,15 +115,15 @@ object StringMatrixSpec extends ZIOSpecDefault { val genTestStrings = for { - n <- Gen.int(1, 63) + n <- Gen.int(1, 64) xs <- Gen.setOfN(n)(genNonEmptyString) } yield xs.toList val genTestStringsAndAliases = for { - xsn <- Gen.int(1, 63) + xsn <- Gen.int(1, 64) xs <- Gen.setOfN(xsn)(genNonEmptyString) - an <- Gen.int(0, 63 - xsn) + an <- Gen.int(0, 64 - xsn) aliasF <- Gen.setOfN(an)(genNonEmptyString.filter(a => !xs.contains(a))).map(_.toList) aliasN <- Gen.listOfN(an)(Gen.int(0, xsn - 1)) } yield (xs.toList, aliasF zip aliasN) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 39dc7fd2a..738363d6f 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -409,35 +409,39 @@ private final class EscapedString(trace: List[JsonError], in: OneCharReader) ext final class StringMatrix(val xs: Array[String], aliases: Array[(String, Int)] = Array.empty) { require(xs.forall(_.nonEmpty)) require(xs.nonEmpty) - require(xs.length + aliases.length < 64) require(aliases.forall(_._1.nonEmpty)) require(aliases.forall(p => p._2 >= 0 && p._2 < xs.length)) - val width = xs.length + aliases.length - val height: Int = xs.map(_.length).max max (if (aliases.isEmpty) 0 else aliases.map(_._1.length).max) - val lengths: Array[Int] = xs.map(_.length) ++ aliases.map(_._1.length) - val initial: Long = (0 until width).foldLeft(0L)((bs, r) => bs | (1L << r)) + val width: Int = xs.length + aliases.length - private val matrix: Array[Int] = { - val m = Array.fill[Int](width * height)(-1) - var string: Int = 0 + require(width <= 64) + + val lengths: Array[Int] = Array.tabulate[Int](width) { string => + if (string < xs.length) xs(string).length + else aliases(string - xs.length)._1.length + } + val height: Int = lengths.max + val initial: Long = -1L >>> (64 - width) + private val matrix: Array[Char] = { + val m = Array.fill[Char](width * height)(0xffff) + var string = 0 while (string < width) { - val s = if (string < xs.length) xs(string) else aliases(string - xs.length)._1 - val len = s.length - var char: Int = 0 + val s = + if (string < xs.length) xs(string) + else aliases(string - xs.length)._1 + val len = s.length + var char = 0 while (char < len) { - m(width * char + string) = s.codePointAt(char) + m(width * char + string) = s.charAt(char) char += 1 } string += 1 } m } - - private val resolve: Array[Int] = { - val r = Array.tabulate[Int](xs.length + aliases.length)(identity) - aliases.zipWithIndex.foreach { case ((_, pi), i) => r(xs.length + i) = pi } - r + private val resolve: Array[Byte] = Array.tabulate[Byte](width) { string => + if (string < xs.length) string.toByte + else aliases(string - xs.length)._2.toByte } // must be called with increasing `char` (starting with bitset obtained from a @@ -446,27 +450,23 @@ final class StringMatrix(val xs: Array[String], aliases: Array[(String, Int)] = if (char >= height) 0L // too long else if (bitset == 0L) 0L // everybody lost else { - var latest: Long = bitset - val base: Int = width * char - + var latest = bitset + val base = width * char if (bitset == initial) { // special case when it is dense since it is simple - var string: Int = 0 + var string = 0 while (string < width) { - if (matrix(base + string) != c) - latest = latest ^ (1L << string) + if (matrix(base + string) != c) latest ^= 1L << string string += 1 } } else { - var remaining: Long = bitset + var remaining = bitset while (remaining != 0L) { - val string: Int = java.lang.Long.numberOfTrailingZeros(remaining) - val bit: Long = 1L << string - if (matrix(base + string) != c) - latest = latest ^ bit - remaining = remaining ^ bit + val string = java.lang.Long.numberOfTrailingZeros(remaining) + val bit = 1L << string + if (matrix(base + string) != c) latest ^= bit + remaining ^= bit } } - latest } @@ -474,14 +474,13 @@ final class StringMatrix(val xs: Array[String], aliases: Array[(String, Int)] = def exact(bitset: Long, length: Int): Long = if (length > height) 0L // too long else { - var latest: Long = bitset - var remaining: Long = bitset + var latest = bitset + var remaining = bitset while (remaining != 0L) { - val string: Int = java.lang.Long.numberOfTrailingZeros(remaining) - val bit: Long = 1L << string - if (lengths(string) != length) - latest = latest ^ bit - remaining = remaining ^ bit + val string = java.lang.Long.numberOfTrailingZeros(remaining) + val bit = 1L << string + if (lengths(string) != length) latest ^= bit + remaining ^= bit } latest } From e604945075ce587fcb924536c19b030968421978 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 13 Jan 2025 15:39:37 +0100 Subject: [PATCH 067/311] Faster skipping of JSON values (#1214) --- .../main/scala/zio/json/internal/lexer.scala | 66 ++++++++++++------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 738363d6f..ee9ed00cc 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -103,32 +103,17 @@ object Lexer { matrix.first(bs) } - private[this] val ull: Array[Char] = "ull".toCharArray private[this] val alse: Array[Char] = "alse".toCharArray private[this] val rue: Array[Char] = "rue".toCharArray def skipValue(trace: List[JsonError], in: RetractReader): Unit = (in.nextNonWhitespace(): @switch) match { - case 'n' => readChars(trace, in, ull, "null") - case 'f' => readChars(trace, in, alse, "false") - case 't' => readChars(trace, in, rue, "true") - case '{' => - if (firstField(trace, in)) { - while ({ - { - char(trace, in, '"') - skipString(trace, in) - char(trace, in, ':') - skipValue(trace, in) - }; nextField(trace, in) - }) () - } - case '[' => - if (firstArrayElement(in)) { - while ({ skipValue(trace, in); nextArrayElement(trace, in) }) () - } + case 'n' | 't' => skipFixedChars(in, 3) + case 'f' => skipFixedChars(in, 4) + case '{' => skipObject(in, 0) + case '[' => skipArray(in, 0) case '"' => - skipString(trace, in) + skipString(in, evenBackSlashes = true) case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => skipNumber(in) case c => throw UnsafeJson(JsonError.Message(s"unexpected '$c'") :: trace) @@ -139,10 +124,43 @@ object Lexer { in.retract() } - def skipString(trace: List[JsonError], in: OneCharReader): Unit = { - val stream = new EscapedString(trace, in) - var i: Int = 0 - while ({ i = stream.read(); i != -1 }) () + def skipString(trace: List[JsonError], in: OneCharReader): Unit = + skipString(in, evenBackSlashes = true) + + @tailrec + private def skipFixedChars(in: OneCharReader, n: Int): Unit = + if (n > 0) { + in.readChar() + skipFixedChars(in, n - 1) + } + + @tailrec + private def skipString(in: OneCharReader, evenBackSlashes: Boolean): Unit = + if (evenBackSlashes) { + val ch = in.readChar() + if (ch != '"') skipString(in, ch != '\\') + } else skipString(in, evenBackSlashes = true) + + @tailrec + private def skipObject(in: OneCharReader, level: Int): Unit = { + val ch = in.readChar() + if (ch == '"') { + skipString(in, evenBackSlashes = true) + skipObject(in, level) + } else if (ch == '{') skipObject(in, level + 1) + else if (ch != '}') skipObject(in, level) + else if (level != 0) skipObject(in, level - 1) + } + + @tailrec + private def skipArray(in: OneCharReader, level: Int): Unit = { + val b = in.readChar() + if (b == '"') { + skipString(in, evenBackSlashes = true) + skipArray(in, level) + } else if (b == '[') skipArray(in, level + 1) + else if (b != ']') skipArray(in, level) + else if (level != 0) skipArray(in, level - 1) } // useful for embedded documents, e.g. CSV contained inside JSON From 85ab9712f248a3f0d6a41d0fdc96d173c306c17d Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 14 Jan 2025 10:19:15 +0100 Subject: [PATCH 068/311] Add MiMa binary compatibility checks (#1217) --- .github/workflows/ci.yml | 20 +++++++++++++++++++- build.sbt | 5 ++++- project/BuildHelper.scala | 28 +++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d57b98bd3..12e1f0b24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,9 +106,27 @@ jobs: - name: Run tests run: sbt ++${{ matrix.scala }}! test${{ matrix.platform }} + mima_check: + runs-on: ubuntu-20.04 + timeout-minutes: 30 + steps: + - name: Checkout current branch + uses: actions/checkout@v4 + with: + fetch-depth: 300 + - name: Fetch tags + run: git fetch --depth=300 origin +refs/tags/*:refs/tags/* + - name: Setup Java (temurin@21) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + cache: sbt + - run: sbt +mimaReportBinaryIssues + ci: runs-on: ubuntu-20.04 - needs: [lint, mdoc, benchmarks, test] + needs: [lint, mdoc, benchmarks, test, mima_check] steps: - name: Aggregate of lint, and all tests run: echo "ci passed" diff --git a/build.sbt b/build.sbt index 5b5bb35c5..4f8d68d64 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,5 @@ import BuildHelper.* +import com.typesafe.tools.mima.plugin.MimaKeys.mimaPreviousArtifacts import explicitdeps.ExplicitDepsPlugin.autoImport.moduleFilterRemoveValue import sbtcrossproject.CrossPlugin.autoImport.crossProject @@ -60,7 +61,8 @@ val zioVersion = "2.1.14" lazy val zioJsonRoot = project .in(file(".")) .settings( - publish / skip := true, + publish / skip := true, + mimaPreviousArtifacts := Set(), unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library") ) .aggregate( @@ -400,6 +402,7 @@ lazy val docs = project zioJsonInteropScalaz7x.jvm, zioJsonGolden ), + mimaPreviousArtifacts := Set(), readmeAcknowledgement := """|- Uses [JsonTestSuite](https://github.com/nst/JSONTestSuite) to test parsing. (c) 2016 Nicolas Seriot) | diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index bd5f1b296..9d5c3f915 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -1,3 +1,7 @@ +import com.typesafe.tools.mima.core.Problem +import com.typesafe.tools.mima.core.ProblemFilters.exclude +import com.typesafe.tools.mima.plugin.MimaKeys.{ mimaBinaryIssueFilters, mimaFailOnProblem, mimaPreviousArtifacts } +import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport.mimaCheckDirection import explicitdeps.ExplicitDepsPlugin.autoImport.* import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport.* import sbt.* @@ -5,6 +9,7 @@ import sbt.Keys.* import sbtbuildinfo.* import sbtbuildinfo.BuildInfoKeys.* import sbtcrossproject.CrossPlugin.autoImport.* +import sbtdynver.DynVerPlugin.autoImport.previousStableVersion import scala.scalanative.sbtplugin.ScalaNativePlugin.autoImport.* @@ -237,7 +242,28 @@ object BuildHelper { Test / parallelExecution := true, incOptions ~= (_.withLogRecompileOnMacro(false)), autoAPIMappings := true, - unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library") + unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library"), + mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% name.value % _).toSet, + mimaCheckDirection := { + def isPatch: Boolean = { + val Array(newMajor, newMinor, _) = version.value.split('.') + val Array(oldMajor, oldMinor, _) = previousStableVersion.value.getOrElse(version.value).split('.') + newMajor == oldMajor && newMinor == oldMinor + } + + if (isPatch) "both" + else "backward" + }, + mimaBinaryIssueFilters ++= Seq( + exclude[Problem]("zio.json.macros#package."), + exclude[Problem]("zio.JsonPackagePlatformSpecific.*"), + exclude[Problem]("zio.json.JsonDecoderPlatformSpecific.*"), + exclude[Problem]("zio.json.JsonEncoderPlatformSpecific.*"), + exclude[Problem]("zio.json.internal.*"), + exclude[Problem]("zio.json.package.*"), + exclude[Problem]("zio.json.yaml.internal.*") + ), + mimaFailOnProblem := true ) def macroExpansionSettings = Seq( From a34ebdcfb70cbd76be2418f50460f934d379a4fa Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 15 Jan 2025 12:28:59 +0100 Subject: [PATCH 069/311] Restore tests for zio-json-interop-refined (#1218) --- .jvmopts | 6 ++--- build.sbt | 24 +++++++++---------- .../json/interop/refined/RefinedSpec.scala | 8 ++++--- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.jvmopts b/.jvmopts index 26e6a558b..411a33b13 100644 --- a/.jvmopts +++ b/.jvmopts @@ -1,6 +1,6 @@ --Xmx4g +-Xmx6g -Xss2m --XX:MaxMetaspaceSize=1g +-XX:+UseG1GC +-XX:InitialCodeCacheSize=512m -XX:ReservedCodeCacheSize=512m --XX:+UseParallelGC -Dfile.encoding=UTF8 diff --git a/build.sbt b/build.sbt index 4f8d68d64..bd366573c 100644 --- a/build.sbt +++ b/build.sbt @@ -28,32 +28,32 @@ addCommandAlias("prepare", "fmt") addCommandAlias( "testJVM", - "zioJsonJVM/test; zioJsonYaml/test; zioJsonInteropHttp4s/test; zioJsonInteropScalaz7xJVM/test; zioJsonGolden/test" + "zioJsonJVM/test; zioJsonYaml/test; zioJsonInteropHttp4s/test; zioJsonInteropScalaz7xJVM/test; zioJsonGolden/test; zioJsonInteropRefinedJVM/test" ) addCommandAlias( - "testScala2JVM", - "zioJsonMacrosJVM/test; zioJsonInteropRefinedJVM/test" + "testJS", + "zioJsonJS/test; zioJsonInteropScalaz7xJS/test; zioJsonInteropRefinedJS/test" ) addCommandAlias( - "testScala2JS", - "zioJsonMacrosJS/test; zioJsonInteropRefinedJS/test" + "testNative", + "zioJsonNative/test; zioJsonInteropScalaz7xNative/test; zioJsonInteropRefinedNative/test" ) addCommandAlias( - "testScala2Native", - "zioJsonMacrosNative/test; zioJsonInteropRefinedNative/test" + "testScala2JVM", + "zioJsonMacrosJVM/test" ) addCommandAlias( - "testJS", - "zioJsonJS/test; zioJsonInteropScalaz7xJS/test" + "testScala2JS", + "zioJsonMacrosJS/test" ) addCommandAlias( - "testNative", - "zioJsonNative/test; zioJsonInteropScalaz7xNative/test" + "testScala2Native", + "zioJsonMacrosNative/test" ) val zioVersion = "2.1.14" @@ -351,7 +351,7 @@ lazy val zioJsonInteropRefined = crossProject(JSPlatform, JVMPlatform, NativePla .settings(buildInfoSettings("zio.json.interop.refined")) .settings( libraryDependencies ++= Seq( - "eu.timepit" %%% "refined" % "0.11.2", + "eu.timepit" %%% "refined" % "0.11.3", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test" ), diff --git a/zio-json-interop-refined/shared/src/test/scala/zio/json/interop/refined/RefinedSpec.scala b/zio-json-interop-refined/shared/src/test/scala/zio/json/interop/refined/RefinedSpec.scala index 50dc8700c..63d4fa052 100644 --- a/zio-json-interop-refined/shared/src/test/scala/zio/json/interop/refined/RefinedSpec.scala +++ b/zio-json-interop-refined/shared/src/test/scala/zio/json/interop/refined/RefinedSpec.scala @@ -1,8 +1,8 @@ package zio.json.interop.refined import eu.timepit.refined.api.Refined -import eu.timepit.refined.auto._ import eu.timepit.refined.collection.NonEmpty +import eu.timepit.refined.types.string.NonEmptyString import zio.json._ import zio.test.Assertion._ import zio.test._ @@ -11,9 +11,11 @@ object RefinedSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = suite("Refined")( test("Refined") { + val person = Person(NonEmptyString.unsafeFrom("fommil")) + val validJson = """{"name":"fommil"}""" assert("""{"name":""}""".fromJson[Person])(isLeft(equalTo(".name(Predicate isEmpty() did not fail.)"))) && - assert("""{"name":"fommil"}""".fromJson[Person])(isRight(equalTo(Person("fommil")))) && - assert(Person("fommil").toJson)(equalTo("""{"name":"fommil"}""")) + assert(validJson.fromJson[Person])(isRight(equalTo(person))) && + assert(person.toJson)(equalTo(validJson)) } ) From ee0d363f94ccdbbaf3b494ec335ff3cf290b8add Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 15 Jan 2025 16:30:17 +0100 Subject: [PATCH 070/311] Update dependencies (#1219) --- build.sbt | 12 ++++++------ project/BuildHelper.scala | 11 +---------- project/NeoJmhPlugin.scala | 2 +- project/plugins.sbt | 11 +++++------ 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/build.sbt b/build.sbt index bd366573c..1a0da59ef 100644 --- a/build.sbt +++ b/build.sbt @@ -127,8 +127,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.10", "io.circe" %%% "circe-generic-extras" % "0.14.4" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.9" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.9" % "test" + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.0" % "test" ) } }, @@ -236,13 +236,13 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) if (n >= 13) { Seq( "com.particeep" %% "play-json-extensions" % "0.43.1" % "test", - "com.typesafe.play" %%% "play-json" % "2.9.4" % "test", + "com.typesafe.play" %%% "play-json" % "2.10.6" % "test", "org.typelevel" %% "jawn-ast" % "1.6.0" % "test" ) } else { Seq( "ai.x" %% "play-json-extensions" % "0.42.0" % "test", - "com.typesafe.play" %%% "play-json" % "2.9.4" % "test", + "com.typesafe.play" %%% "play-json" % "2.10.6" % "test", "org.typelevel" %% "jawn-ast" % "1.6.0" % "test" ) } @@ -332,9 +332,9 @@ lazy val zioJsonInteropHttp4s = project .settings( crossScalaVersions -= ScalaDotty, libraryDependencies ++= Seq( - "org.http4s" %% "http4s-dsl" % "0.23.29", + "org.http4s" %% "http4s-dsl" % "0.23.30", "dev.zio" %% "zio" % zioVersion, - "org.typelevel" %% "cats-effect" % "3.4.9", + "org.typelevel" %% "cats-effect" % "3.5.7", "dev.zio" %% "zio-interop-cats" % "23.1.0.3" % "test", "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 9d5c3f915..0e5a40a35 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -244,16 +244,7 @@ object BuildHelper { autoAPIMappings := true, unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library"), mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% name.value % _).toSet, - mimaCheckDirection := { - def isPatch: Boolean = { - val Array(newMajor, newMinor, _) = version.value.split('.') - val Array(oldMajor, oldMinor, _) = previousStableVersion.value.getOrElse(version.value).split('.') - newMajor == oldMajor && newMinor == oldMinor - } - - if (isPatch) "both" - else "backward" - }, + mimaCheckDirection := "backward", // TODO: find how we can use "both" for path versions mimaBinaryIssueFilters ++= Seq( exclude[Problem]("zio.json.macros#package."), exclude[Problem]("zio.JsonPackagePlatformSpecific.*"), diff --git a/project/NeoJmhPlugin.scala b/project/NeoJmhPlugin.scala index bc5fd6e81..6edc79ed0 100644 --- a/project/NeoJmhPlugin.scala +++ b/project/NeoJmhPlugin.scala @@ -43,7 +43,7 @@ object NeoJmhPlugin extends AutoPlugin { override def projectConfigurations = Seq(Jmh, JmhInternal) override def buildSettings = Seq( - jmhVersion := "1.36", + jmhVersion := "1.37", jmhExtrasVersion := "0.3.7" ) diff --git a/project/plugins.sbt b/project/plugins.sbt index 2ffed054f..2d491132c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,16 +1,15 @@ -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.0") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.6.1") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.1") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.3") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") -addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.28") -addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.27") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.30") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.8" From b2d74c938131dd35dbea566ca38de950519c9d9a Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 16 Jan 2025 13:10:22 +0100 Subject: [PATCH 071/311] Clean up zio-json core tests and benchmarks (#1220) * Align package names with directories * Remove unused imports * Remove unused play-json codecs * Add testing of platform specific API with Scala 3 * Add testing of internal API with Scala.js and Scala Native * Formatting * Remove benchmarks for play-json * Add checks for benchmark compilation using Scala 3 * Use the same time unit for all benchmarks * Fix stack overflow for synthetic circe benchmarks * Add encoder benchmarks for jsoniter-scala * Increase timeout for the interruption test --- .github/workflows/ci.yml | 2 +- build.sbt | 61 ++---- .../main/scala/zio/json/golden/package.scala | 2 - .../src/test/scala/zio/json/DeriveSpec.scala | 3 +- .../json/CollectionDecoderBenchmarks.scala | 1 - .../json/CollectionEncoderBenchmarks.scala | 1 - .../scala/zio/json/GeoJSONBenchmarks.scala | 48 +---- .../zio/json/GoogleMapsAPIBenchmarks.scala | 113 +++-------- .../scala/zio/json/SyntheticBenchmarks.scala | 39 +--- .../scala/zio/json/TwitterAPIBenchmarks.scala | 46 ++--- .../scala-2/zio/json/data/GoogleMaps.scala | 110 ---------- .../json/DecoderPlatformSpecificSpec.scala | 21 +- .../json/EncoderPlatformSpecificSpec.scala | 10 +- .../scala/zio/json/JsonTestSuiteSpec.scala | 5 +- .../src/test/scala/zio/json/TestUtils.scala | 2 +- .../zio/json/data/geojson}/GeoJSON.scala | 190 +++++++++++------- .../zio/json/data/googlemaps/GoogleMaps.scala | 54 +++++ .../zio/json/data/twitter}/Twitter.scala | 82 ++------ .../scala-3/zio/json/DerivedCodecSpec.scala | 3 +- .../scala-3/zio/json/DerivedDecoderSpec.scala | 3 +- .../scala-3/zio/json/DerivedEncoderSpec.scala | 3 +- .../scala/zio/json/AnnotationsCodecSpec.scala | 1 - .../src/test/scala/zio/json/CarterSpec.scala | 3 +- .../src/test/scala/zio/json/CodecSpec.scala | 4 +- .../src/test/scala/zio/json/DecoderSpec.scala | 13 +- .../src/test/scala/zio/json/EncoderSpec.scala | 5 +- .../shared/src/test/scala/zio/json/Gens.scala | 2 +- .../test/scala/zio/json/JavaTimeSpec.scala | 5 +- .../test/scala/zio/json/RoundTripSpec.scala | 5 +- .../zio/json/internal/SafeNumbersSpec.scala | 8 +- .../zio/json/internal/StringMatrixSpec.scala | 85 ++++---- 31 files changed, 352 insertions(+), 578 deletions(-) delete mode 100644 zio-json/jvm/src/test/scala-2/zio/json/data/GoogleMaps.scala rename zio-json/jvm/src/test/{scala-2 => scala}/zio/json/DecoderPlatformSpecificSpec.scala (96%) rename zio-json/jvm/src/test/{scala-2 => scala}/zio/json/EncoderPlatformSpecificSpec.scala (95%) rename zio-json/jvm/src/test/{scala-2/zio/json/data => scala/zio/json/data/geojson}/GeoJSON.scala (68%) create mode 100644 zio-json/jvm/src/test/scala/zio/json/data/googlemaps/GoogleMaps.scala rename zio-json/jvm/src/test/{scala-2/zio/json/data => scala/zio/json/data/twitter}/Twitter.scala (57%) rename zio-json/{jvm => shared}/src/test/scala/zio/json/CarterSpec.scala (98%) rename zio-json/{jvm => shared}/src/test/scala/zio/json/internal/SafeNumbersSpec.scala (98%) rename zio-json/{jvm => shared}/src/test/scala/zio/json/internal/StringMatrixSpec.scala (62%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12e1f0b24..b7b8f34ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: java: ['17', '21'] - scala: ['2.13.15'] + scala: ['2.13.15', '3.3.4'] steps: - name: Checkout current branch uses: actions/checkout@v4.1.2 diff --git a/build.sbt b/build.sbt index 1a0da59ef..fe67132b8 100644 --- a/build.sbt +++ b/build.sbt @@ -100,35 +100,34 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) scalacOptions -= "-opt-inline-from:zio.internal.**", Test / scalacOptions ++= { if (scalaVersion.value == ScalaDotty) - Vector("-Yretain-trees") + Vector("-Yretain-trees", "-Xmax-inlines:100") else Vector.empty }, libraryDependencies ++= Seq( - "dev.zio" %%% "zio" % zioVersion, - "dev.zio" %%% "zio-streams" % zioVersion, - "org.scala-lang.modules" %%% "scala-collection-compat" % "2.12.0", - "dev.zio" %%% "zio-test" % zioVersion % "test", - "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "io.circe" %%% "circe-core" % circeVersion % "test", - "io.circe" %%% "circe-generic" % circeVersion % "test", - "io.circe" %%% "circe-parser" % circeVersion % "test" + "dev.zio" %%% "zio" % zioVersion, + "dev.zio" %%% "zio-streams" % zioVersion, + "org.scala-lang.modules" %%% "scala-collection-compat" % "2.12.0", + "dev.zio" %%% "zio-test" % zioVersion % "test", + "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.0" % "test", + "io.circe" %%% "circe-core" % circeVersion % "test", + "io.circe" %%% "circe-generic" % circeVersion % "test", + "io.circe" %%% "circe-parser" % circeVersion % "test", + "org.typelevel" %%% "jawn-ast" % "1.6.0" % "test" ), // scala version specific dependencies libraryDependencies ++= { CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => - Vector( + Seq( "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.7" ) - case _ => - Vector( - "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, - "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.10", - "io.circe" %%% "circe-generic-extras" % "0.14.4" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.0" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.0" % "test" + Seq( + "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, + "com.softwaremill.magnolia1_2" %%% "magnolia" % "1.1.10" ) } }, @@ -224,34 +223,6 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion ) ) - .jvmSettings( - libraryDependencies ++= { - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((3, _)) => - Vector( - "org.typelevel" %% "jawn-ast" % "1.6.0" % "test" - ) - - case Some((2, n)) => - if (n >= 13) { - Seq( - "com.particeep" %% "play-json-extensions" % "0.43.1" % "test", - "com.typesafe.play" %%% "play-json" % "2.10.6" % "test", - "org.typelevel" %% "jawn-ast" % "1.6.0" % "test" - ) - } else { - Seq( - "ai.x" %% "play-json-extensions" % "0.42.0" % "test", - "com.typesafe.play" %%% "play-json" % "2.10.6" % "test", - "org.typelevel" %% "jawn-ast" % "1.6.0" % "test" - ) - } - - case _ => - Seq.empty - } - } - ) .nativeSettings(nativeSettings) .nativeSettings( libraryDependencies ++= Seq( diff --git a/zio-json-golden/src/main/scala/zio/json/golden/package.scala b/zio-json-golden/src/main/scala/zio/json/golden/package.scala index 8bdf6aec8..561aae7bd 100644 --- a/zio-json-golden/src/main/scala/zio/json/golden/package.scala +++ b/zio-json-golden/src/main/scala/zio/json/golden/package.scala @@ -1,7 +1,5 @@ package zio.json -import scala.annotation.nowarn - import zio.Tag import zio.{ test => _, _ } import zio.json.golden.filehelpers._ diff --git a/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala b/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala index fe1d95847..b09994a45 100644 --- a/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala +++ b/zio-json-macros/shared/src/test/scala/zio/json/DeriveSpec.scala @@ -1,6 +1,5 @@ -package testzio.json +package zio.json -import zio.json._ import zio.test.Assertion._ import zio.test._ diff --git a/zio-json/jvm/src/jmh/scala/zio/json/CollectionDecoderBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/CollectionDecoderBenchmarks.scala index 1d87aa2d8..968e75d1b 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/CollectionDecoderBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/CollectionDecoderBenchmarks.scala @@ -10,7 +10,6 @@ import scala.collection.immutable @State(Scope.Thread) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@OutputTimeUnit(TimeUnit.MILLISECONDS) @Fork(value = 1) class CollectionDecoderBenchmarks { private[this] var encodedArray: String = null diff --git a/zio-json/jvm/src/jmh/scala/zio/json/CollectionEncoderBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/CollectionEncoderBenchmarks.scala index 226d46ab6..46b9ad860 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/CollectionEncoderBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/CollectionEncoderBenchmarks.scala @@ -10,7 +10,6 @@ import scala.collection.{ SortedMap, immutable } @State(Scope.Thread) @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@OutputTimeUnit(TimeUnit.MILLISECONDS) @Fork(value = 1) class CollectionEncoderBenchmarks { private[this] var stringsChunk: Chunk[String] = null diff --git a/zio-json/jvm/src/jmh/scala/zio/json/GeoJSONBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/GeoJSONBenchmarks.scala index 3c818e917..e8a407be9 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/GeoJSONBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/GeoJSONBenchmarks.scala @@ -1,16 +1,14 @@ package zio.json -import java.nio.charset.StandardCharsets._ import java.util.concurrent.TimeUnit import com.github.plokhotnyuk.jsoniter_scala.core._ import com.github.plokhotnyuk.jsoniter_scala.macros._ import io.circe import zio.json.GeoJSONBenchmarks._ -import testzio.json.TestUtils._ -import testzio.json.data.geojson.handrolled._ +import zio.json.TestUtils._ +import zio.json.data.geojson.handrolled._ import org.openjdk.jmh.annotations._ -import play.api.libs.{ json => Play } import scala.util.Try @@ -42,30 +40,26 @@ class GeoJSONBenchmarks { assert(decodeCirceSuccess2() == decodeZioSuccess2()) assert(decodeCirceError().isLeft) - // these are failing because of a bug in play-json, but they succeed - // assert(decodeCirceSuccess1() == decodePlaySuccess1(), decodePlaySuccess1().toString) - // assert(decodeCirceSuccess2() == decodePlaySuccess2()) - assert(decodePlaySuccess1().isRight) - assert(decodePlaySuccess2().isRight) - assert(decodePlayError().isLeft) - assert(decodeZioError().isLeft) } @Benchmark def decodeJsoniterSuccess1(): Either[String, GeoJSON] = - Try(readFromArray(jsonString1.getBytes(UTF_8))) - .fold(t => Left(t.toString), Right.apply) + Try(readFromString(jsonString1)) + .fold(t => Left(t.toString), Right.apply(_)) @Benchmark def decodeJsoniterSuccess2(): Either[String, GeoJSON] = - Try(readFromArray(jsonString2.getBytes(UTF_8))) - .fold(t => Left(t.toString), Right.apply) + Try(readFromString(jsonString2)) + .fold(t => Left(t.toString), Right.apply(_)) @Benchmark def decodeJsoniterError(): Either[String, GeoJSON] = - Try(readFromArray(jsonStringErr.getBytes(UTF_8))) - .fold(t => Left(t.toString), Right.apply) + Try(readFromString(jsonStringErr)) + .fold(t => Left(t.toString), Right.apply(_)) + + @Benchmark + def encodeJsoniter(): String = writeToString(decoded) @Benchmark def decodeCirceSuccess1(): Either[circe.Error, GeoJSON] = @@ -86,25 +80,6 @@ class GeoJSONBenchmarks { def decodeCirceError(): Either[circe.Error, GeoJSON] = circe.parser.decode[GeoJSON](jsonStringErr) - @Benchmark - def decodePlaySuccess1(): Either[String, GeoJSON] = - Try(Play.Json.parse(jsonString1).as[GeoJSON]) - .fold(t => Left(t.toString), Right.apply) - - @Benchmark - def decodePlaySuccess2(): Either[String, GeoJSON] = - Try(Play.Json.parse(jsonString2).as[GeoJSON]) - .fold(t => Left(t.toString), Right.apply) - - @Benchmark - def encodePlay(): String = - Play.Json.stringify(implicitly[Play.Writes[GeoJSON]].writes(decoded)) - - @Benchmark - def decodePlayError(): Either[String, GeoJSON] = - Try(Play.Json.parse(jsonStringErr).as[GeoJSON]) - .fold(t => Left(t.toString), Right.apply) - @Benchmark def decodeZioSuccess1(): Either[String, GeoJSON] = jsonChars1.fromJson[GeoJSON] @@ -120,7 +95,6 @@ class GeoJSONBenchmarks { @Benchmark def decodeZioError(): Either[String, GeoJSON] = jsonCharsErr.fromJson[GeoJSON] - } object GeoJSONBenchmarks { diff --git a/zio-json/jvm/src/jmh/scala/zio/json/GoogleMapsAPIBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/GoogleMapsAPIBenchmarks.scala index d8e994145..963567960 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/GoogleMapsAPIBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/GoogleMapsAPIBenchmarks.scala @@ -1,15 +1,14 @@ package zio.json -import java.util.Arrays import java.util.concurrent.TimeUnit import com.github.plokhotnyuk.jsoniter_scala.core._ import com.github.plokhotnyuk.jsoniter_scala.macros._ import io.circe -import testzio.json.TestUtils._ -import testzio.json.data.googlemaps._ +import zio.json.TestUtils._ +import zio.json.data.googlemaps._ import org.openjdk.jmh.annotations._ -import play.api.libs.{ json => Play } +import zio.json.GoogleMapsAPIBenchmarks._ import scala.util.Try @@ -94,51 +93,49 @@ class GoogleMapsAPIBenchmarks { assert(decodeCirceSuccess1() == decodeZioSuccess1()) assert(decodeCirceSuccess2() == decodeZioSuccess2()) - assert(decodeCirceSuccess1() == decodePlaySuccess1()) - assert(decodeCirceSuccess2() == decodePlaySuccess2()) assert(decodeCirceSuccess1() == decodeCirceAttack0()) assert(decodeCirceSuccess1() == decodeZioAttack0()) - assert(decodeCirceSuccess1() == decodePlayAttack0()) assert(decodeCirceSuccess1() == decodeCirceAttack1()) assert(decodeCirceSuccess1() == decodeZioAttack1()) - assert(decodeCirceSuccess1() == decodePlayAttack1()) assert(decodeCirceSuccess1() == decodeCirceAttack2()) assert(decodeCirceSuccess1() == decodeZioAttack2()) - assert(decodeCirceSuccess1() == decodePlayAttack2()) } - // @Benchmark - // def decodeJsoniterSuccess1(): Either[String, DistanceMatrix] = - // Try(readFromArray(jsonString.getBytes(UTF_8))) - // .fold(t => Left(t.toString), Right.apply) + @Benchmark + def decodeJsoniterSuccess1(): Either[String, DistanceMatrix] = + Try(readFromString(jsonString)) + .fold(t => Left(t.toString), Right.apply(_)) - // @Benchmark - // def decodeJsoniterSuccess2(): Either[String, DistanceMatrix] = - // Try(readFromArray(jsonStringCompact.getBytes(UTF_8))) - // .fold(t => Left(t.toString), Right.apply) + @Benchmark + def decodeJsoniterSuccess2(): Either[String, DistanceMatrix] = + Try(readFromString(jsonStringCompact)) + .fold(t => Left(t.toString), Right.apply(_)) - // @Benchmark - // def decodeJsoniterError(): Either[String, DistanceMatrix] = - // Try(readFromArray(jsonStringErr.getBytes(UTF_8))) - // .fold(t => Left(t.toString), Right.apply) + @Benchmark + def decodeJsoniterError(): Either[String, DistanceMatrix] = + Try(readFromString(jsonStringErr)) + .fold(t => Left(t.toString), Right.apply(_)) - // @Benchmark - // def decodeJsoniterAttack1(): Either[String, DistanceMatrix] = - // Try(readFromArray(jsonStringAttack1.getBytes(UTF_8))) - // .fold(t => Left(t.toString), Right.apply) + @Benchmark + def decodeJsoniterAttack1(): Either[String, DistanceMatrix] = + Try(readFromString(jsonStringAttack1)) + .fold(t => Left(t.toString), Right.apply(_)) - // @Benchmark - // def decodeJsoniterAttack2(): Either[String, DistanceMatrix] = - // Try(readFromArray(jsonStringAttack2.getBytes(UTF_8))) - // .fold(t => Left(t.toString), Right.apply) + @Benchmark + def decodeJsoniterAttack2(): Either[String, DistanceMatrix] = + Try(readFromString(jsonStringAttack2)) + .fold(t => Left(t.toString), Right.apply(_)) - // @Benchmark - // def decodeJsoniterAttack3(): Either[String, DistanceMatrix] = - // Try(readFromArray(jsonStringAttack3.getBytes(UTF_8))) - // .fold(t => Left(t.toString), Right.apply) + @Benchmark + def decodeJsoniterAttack3(): Either[String, DistanceMatrix] = + Try(readFromString(jsonStringAttack3)) + .fold(t => Left(t.toString), Right.apply(_)) + + @Benchmark + def encodeJsoniter(): String = writeToString(decoded) @Benchmark def decodeCirceSuccess1(): Either[circe.Error, DistanceMatrix] = @@ -183,56 +180,6 @@ class GoogleMapsAPIBenchmarks { def decodeCirceAttack3(): Either[circe.Error, DistanceMatrix] = circe.parser.decode[DistanceMatrix](jsonStringAttack3) - def playDecode[A]( - str: String - )(implicit R: Play.Reads[A]): Either[String, A] = - Try(Play.Json.parse(str).as[A]).fold( - // if we don't access the stacktrace then the JVM can optimise it away in - // these tight loop perf tests, which would cover up a real bottleneck - err => Left(Arrays.toString(err.getStackTrace().asInstanceOf[Array[Object]])), - a => Right(a) - ) - - @Benchmark - def decodePlaySuccess1(): Either[String, DistanceMatrix] = - playDecode[DistanceMatrix](jsonString) - - @Benchmark - def decodePlaySuccess2(): Either[String, DistanceMatrix] = - playDecode[DistanceMatrix](jsonStringCompact) - - @Benchmark - def encodePlay(): String = - Play.Json.stringify(implicitly[Play.Writes[DistanceMatrix]].writes(decoded)) - - // @Benchmark - // def decodePlayError(): Either[String, DistanceMatrix] = - // playDecode[DistanceMatrix](jsonStringErr) - - @Benchmark - def decodePlayErrorParse(): Either[String, DistanceMatrix] = - playDecode[DistanceMatrix](jsonStringErrParse) - - @Benchmark - def decodePlayErrorNumber(): Either[String, DistanceMatrix] = - playDecode[DistanceMatrix](jsonStringErrNumber) - - @Benchmark - def decodePlayAttack0(): Either[String, DistanceMatrix] = - playDecode[DistanceMatrix](jsonStringAttack0) - - @Benchmark - def decodePlayAttack1(): Either[String, DistanceMatrix] = - playDecode[DistanceMatrix](jsonStringAttack1) - - @Benchmark - def decodePlayAttack2(): Either[String, DistanceMatrix] = - playDecode[DistanceMatrix](jsonStringAttack2) - - @Benchmark - def decodePlayAttack3(): Either[String, DistanceMatrix] = - playDecode[DistanceMatrix](jsonStringAttack3) - @Benchmark def decodeZioSuccess1(): Either[String, DistanceMatrix] = jsonChars.fromJson[DistanceMatrix] diff --git a/zio-json/jvm/src/jmh/scala/zio/json/SyntheticBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/SyntheticBenchmarks.scala index 1f04cd89c..b6b529e65 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/SyntheticBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/SyntheticBenchmarks.scala @@ -1,15 +1,15 @@ package zio.json -import java.nio.charset.StandardCharsets.UTF_8 import java.util.concurrent.TimeUnit import com.github.plokhotnyuk.jsoniter_scala.core._ import com.github.plokhotnyuk.jsoniter_scala.macros._ import io.circe +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec import zio.json.SyntheticBenchmarks._ -import testzio.json.TestUtils._ +import zio.json.TestUtils._ import org.openjdk.jmh.annotations._ -import play.api.libs.{ json => Play } import scala.util.Try @@ -19,18 +19,8 @@ object Nested { DeriveJsonDecoder.gen implicit lazy val zioJsonEncoder: JsonEncoder[Nested] = DeriveJsonEncoder.gen - - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - .copy(discriminator = Some("type")) - implicit lazy val circeJsonDecoder: circe.Decoder[Nested] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Nested] - implicit lazy val circeEncoder: circe.Encoder[Nested] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Nested] - - implicit lazy val playFormatter: Play.Format[Nested] = - Play.Json.format[Nested] - + implicit lazy val circeCodec: Codec[Nested] = + deriveCodec } @State(Scope.Thread) @@ -39,7 +29,7 @@ object Nested { @Fork(value = 1) class SyntheticBenchmarks { // @Param(Array("100", "1000")) - var size: Int = 500 + var size: Int = 100 var jsonString: String = _ var jsonChars: CharSequence = _ var decoded: Nested = _ @@ -60,15 +50,16 @@ class SyntheticBenchmarks { assert(decodeJsoniterSuccess() == decodeZioSuccess()) assert(decodeCirceSuccess() == decodeZioSuccess()) - - assert(decodePlaySuccess() == decodeZioSuccess()) } @Benchmark def decodeJsoniterSuccess(): Either[String, Nested] = - Try(readFromArray(jsonString.getBytes(UTF_8))) + Try(readFromString(jsonString)) .fold(t => Left(t.toString), Right(_)) + @Benchmark + def encodeJsoniter(): String = writeToString(decoded) + @Benchmark def decodeCirceSuccess(): Either[circe.Error, Nested] = circe.parser.decode[Nested](jsonString) @@ -80,15 +71,6 @@ class SyntheticBenchmarks { decoded.asJson.noSpaces } - @Benchmark - def decodePlaySuccess(): Either[String, Nested] = - Try(Play.Json.parse(jsonString).as[Nested]) - .fold(t => Left(t.toString), Right.apply) - - @Benchmark - def encodePlay(): String = - Play.Json.stringify(implicitly[Play.Writes[Nested]].writes(decoded)) - @Benchmark def decodeZioSuccess(): Either[String, Nested] = jsonChars.fromJson[Nested] @@ -96,7 +78,6 @@ class SyntheticBenchmarks { @Benchmark def encodeZio(): CharSequence = JsonEncoder[Nested].encodeJson(decoded, None) - } object SyntheticBenchmarks { diff --git a/zio-json/jvm/src/jmh/scala/zio/json/TwitterAPIBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/TwitterAPIBenchmarks.scala index 14eb64ed4..42952ae94 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/TwitterAPIBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/TwitterAPIBenchmarks.scala @@ -1,16 +1,14 @@ package zio.json -import java.nio.charset.StandardCharsets.UTF_8 import java.util.concurrent.TimeUnit import com.github.plokhotnyuk.jsoniter_scala.core._ import com.github.plokhotnyuk.jsoniter_scala.macros._ import io.circe -import testzio.json.TestUtils._ -import testzio.json.data.twitter._ +import zio.json.TestUtils._ +import zio.json.data.twitter._ import org.openjdk.jmh.annotations._ -import play.api.libs.{ json => Play } -import TwitterAPIBenchmarks._ +import zio.json.TwitterAPIBenchmarks._ import scala.util.Try @@ -44,27 +42,26 @@ class TwitterAPIBenchmarks { assert(decodeCirceSuccess2() == decodeZioSuccess2()) assert(decodeCirceError().isLeft) - assert(decodePlaySuccess1() == decodeZioSuccess1()) - assert(decodePlaySuccess2() == decodeZioSuccess2()) - assert(decodePlayError().isLeft) - assert(decodeZioError().isLeft) } @Benchmark def decodeJsoniterSuccess1(): Either[String, List[Tweet]] = - Try(readFromArray(jsonString.getBytes(UTF_8))) - .fold(t => Left(t.toString), Right.apply) + Try(readFromString(jsonString)) + .fold(t => Left(t.toString), Right.apply(_)) @Benchmark def decodeJsoniterSuccess2(): Either[String, List[Tweet]] = - Try(readFromArray(jsonStringCompact.getBytes(UTF_8))) - .fold(t => Left(t.toString), Right.apply) + Try(readFromString(jsonStringCompact)) + .fold(t => Left(t.toString), Right.apply(_)) @Benchmark def decodeJsoniterError(): Either[String, List[Tweet]] = - Try(readFromArray(jsonStringErr.getBytes(UTF_8))) - .fold(t => Left(t.toString), Right.apply) + Try(readFromString(jsonStringErr)) + .fold(t => Left(t.toString), Right.apply(_)) + + @Benchmark + def encodeJsoniter(): String = writeToString(decoded) @Benchmark def decodeCirceSuccess1(): Either[circe.Error, List[Tweet]] = @@ -85,25 +82,6 @@ class TwitterAPIBenchmarks { def decodeCirceError(): Either[circe.Error, List[Tweet]] = circe.parser.decode[List[Tweet]](jsonStringErr) - @Benchmark - def decodePlaySuccess1(): Either[String, List[Tweet]] = - Try(Play.Json.parse(jsonString).as[List[Tweet]]) - .fold(t => Left(t.toString), Right.apply) - - @Benchmark - def decodePlaySuccess2(): Either[String, List[Tweet]] = - Try(Play.Json.parse(jsonStringCompact).as[List[Tweet]]) - .fold(t => Left(t.toString), Right.apply) - - @Benchmark - def encodePlay(): String = - Play.Json.stringify(implicitly[Play.Writes[List[Tweet]]].writes(decoded)) - - @Benchmark - def decodePlayError(): Either[String, List[Tweet]] = - Try(Play.Json.parse(jsonStringErr).as[List[Tweet]]) - .fold(t => Left(t.toString), Right.apply) - @Benchmark def decodeZioSuccess1(): Either[String, List[Tweet]] = jsonChars.fromJson[List[Tweet]] diff --git a/zio-json/jvm/src/test/scala-2/zio/json/data/GoogleMaps.scala b/zio-json/jvm/src/test/scala-2/zio/json/data/GoogleMaps.scala deleted file mode 100644 index 654c879ba..000000000 --- a/zio-json/jvm/src/test/scala-2/zio/json/data/GoogleMaps.scala +++ /dev/null @@ -1,110 +0,0 @@ -package testzio.json.data.googlemaps - -import com.github.ghik.silencer.silent -import com.github.plokhotnyuk.jsoniter_scala.macros.named -import io.circe -import play.api.libs.{ json => Play } -import zio.json._ - -final case class Value( - text: String, - @named("value") - @jsonField("value") - @circe.generic.extras.JsonKey("value") - v: Int -) -final case class Elements(distance: Value, duration: Value, status: String) -final case class Rows(elements: List[Elements]) -// @jsonNoExtraFields // entirely mitigates Attack1 -final case class DistanceMatrix( - destination_addresses: List[String], - origin_addresses: List[String], - rows: List[Rows], - status: String -) - -@silent("Block result was adapted via implicit conversion") -object Value { - implicit val zioJsonJsonDecoder: JsonDecoder[Value] = DeriveJsonDecoder.gen[Value] - implicit val zioJsonEncoder: JsonEncoder[Value] = DeriveJsonEncoder.gen[Value] - - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[Value] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Value] - implicit val circeEncoder: circe.Encoder[Value] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Value] - - // play macros don't support custom field - // implicit val playJsonDecoder: Play.Reads[Value] = Play.Json.reads[Value] - - implicit val playJsonDecoder: Play.Reads[Value] = { - import play.api.libs.json._ - import play.api.libs.json.Reads._ - import play.api.libs.functional.syntax._ - - ((JsPath \ "text").read[String].and((JsPath \ "value").read[Int]))( - Value.apply _ - ) - } - implicit val playEncoder: Play.Writes[Value] = { - import play.api.libs.json._ - import play.api.libs.json.Writes._ - import play.api.libs.functional.syntax._ - - ((JsPath \ "text").write[String].and((JsPath \ "value").write[Int]))(unlift(Value.unapply)) - } - -} -@silent("Block result was adapted via implicit conversion") -object Elements { - implicit val zioJsonJsonDecoder: JsonDecoder[Elements] = DeriveJsonDecoder.gen[Elements] - implicit val zioJsonEncoder: JsonEncoder[Elements] = DeriveJsonEncoder.gen[Elements] - - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[Elements] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Elements] - implicit val circeEncoder: circe.Encoder[Elements] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Elements] - - implicit val playJsonDecoder: Play.Reads[Elements] = Play.Json.reads[Elements] - implicit val playEncoder: Play.Writes[Elements] = Play.Json.writes[Elements] - -} -@silent("Block result was adapted via implicit conversion") -object Rows { - implicit val zioJsonJsonDecoder: JsonDecoder[Rows] = DeriveJsonDecoder.gen[Rows] - implicit val zioJsonEncoder: JsonEncoder[Rows] = DeriveJsonEncoder.gen[Rows] - - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[Rows] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Rows] - implicit val circeEncoder: circe.Encoder[Rows] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Rows] - - implicit val playJsonDecoder: Play.Reads[Rows] = Play.Json.reads[Rows] - implicit val playEncoder: Play.Writes[Rows] = Play.Json.writes[Rows] - -} -@silent("Block result was adapted via implicit conversion") -object DistanceMatrix { - implicit val zioJsonJsonDecoder: JsonDecoder[DistanceMatrix] = - DeriveJsonDecoder.gen[DistanceMatrix] - implicit val zioJsonEncoder: JsonEncoder[DistanceMatrix] = - DeriveJsonEncoder.gen[DistanceMatrix] - - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[DistanceMatrix] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[DistanceMatrix] - implicit val circeEncoder: circe.Encoder[DistanceMatrix] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[DistanceMatrix] - - implicit val playJsonDecoder: Play.Reads[DistanceMatrix] = - Play.Json.reads[DistanceMatrix] - implicit val playEncoder: Play.Writes[DistanceMatrix] = - Play.Json.writes[DistanceMatrix] - -} diff --git a/zio-json/jvm/src/test/scala-2/zio/json/DecoderPlatformSpecificSpec.scala b/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala similarity index 96% rename from zio-json/jvm/src/test/scala-2/zio/json/DecoderPlatformSpecificSpec.scala rename to zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala index eab496ca8..bc6a53a32 100644 --- a/zio-json/jvm/src/test/scala-2/zio/json/DecoderPlatformSpecificSpec.scala +++ b/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala @@ -1,13 +1,12 @@ -package testzio.json +package zio.json import io.circe import org.typelevel.jawn.{ ast => jawn } -import testzio.json.TestUtils._ -import testzio.json.data.googlemaps._ -import testzio.json.data.twitter._ import zio._ -import zio.json._ +import zio.json.TestUtils._ import zio.json.ast._ +import zio.json.data.googlemaps._ +import zio.json.data.twitter._ import zio.stream.ZStream import zio.test.Assertion._ import zio.test.TestAspect._ @@ -65,28 +64,28 @@ object DecoderPlatformSpecificSpec extends ZIOSpecDefault { } }, test("geojson1") { - import testzio.json.data.geojson.generated._ + import zio.json.data.geojson.generated._ getResourceAsStringM("che.geo.json").map { str => assert(str.fromJson[GeoJSON])(matchesCirceDecoded[GeoJSON](str)) } }, test("geojson1 alt") { - import testzio.json.data.geojson.handrolled._ + import zio.json.data.geojson.handrolled._ getResourceAsStringM("che.geo.json").map { str => assert(str.fromJson[GeoJSON])(matchesCirceDecoded[GeoJSON](str)) } }, test("geojson2") { - import testzio.json.data.geojson.generated._ + import zio.json.data.geojson.generated._ getResourceAsStringM("che-2.geo.json").map { str => assert(str.fromJson[GeoJSON])(matchesCirceDecoded[GeoJSON](str)) } }, test("geojson2 lowlevel") { - import testzio.json.data.geojson.generated._ + import zio.json.data.geojson.generated._ // this uses a lower level Reader to ensure that the more general recorder // impl is covered by the tests @@ -184,7 +183,7 @@ object DecoderPlatformSpecificSpec extends ZIOSpecDefault { .map { exit => assert(exit)(isInterrupted) } - } @@ timeout(2.seconds) + } @@ timeout(7.seconds) ), suite("Array delimited")( test("decodes single elements") { @@ -338,7 +337,7 @@ object DecoderPlatformSpecificSpec extends ZIOSpecDefault { // Helper function because Circe and Zio-JSON’s Left differ, making tests unnecessary verbose def matchesCirceDecoded[A]( expected: String - )(implicit cDecoder: circe.Decoder[A], eq: Eql[A, A]): Assertion[Either[String, A]] = { + )(implicit cDecoder: circe.Decoder[A]): Assertion[Either[String, A]] = { val cDecoded = circe.parser.decode(expected).left.map(_.toString) diff --git a/zio-json/jvm/src/test/scala-2/zio/json/EncoderPlatformSpecificSpec.scala b/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala similarity index 95% rename from zio-json/jvm/src/test/scala-2/zio/json/EncoderPlatformSpecificSpec.scala rename to zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala index 7479e378b..a731094df 100644 --- a/zio-json/jvm/src/test/scala-2/zio/json/EncoderPlatformSpecificSpec.scala +++ b/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala @@ -1,12 +1,12 @@ package zio.json import io.circe -import testzio.json.TestUtils._ -import testzio.json.data.geojson.generated._ -import testzio.json.data.googlemaps._ -import testzio.json.data.twitter._ import zio.Chunk +import zio.json.TestUtils._ import zio.json.ast.Json +import zio.json.data.geojson.generated._ +import zio.json.data.googlemaps._ +import zio.json.data.twitter._ import zio.stream.{ ZSink, ZStream } import zio.test.Assertion._ import zio.test.{ ZIOSpecDefault, assert, _ } @@ -15,7 +15,7 @@ import java.io.IOException import java.nio.file.Files object EncoderPlatformSpecificSpec extends ZIOSpecDefault { - import testzio.json.DecoderSpec.logEvent._ + import zio.json.DecoderSpec.logEvent._ val spec = suite("Encoder")( diff --git a/zio-json/jvm/src/test/scala/zio/json/JsonTestSuiteSpec.scala b/zio-json/jvm/src/test/scala/zio/json/JsonTestSuiteSpec.scala index a3edd89bc..d2408b62f 100644 --- a/zio-json/jvm/src/test/scala/zio/json/JsonTestSuiteSpec.scala +++ b/zio-json/jvm/src/test/scala/zio/json/JsonTestSuiteSpec.scala @@ -1,8 +1,7 @@ -package testzio.json +package zio.json -import testzio.json.TestUtils._ +import zio.json.TestUtils._ import zio._ -import zio.json._ import zio.json.ast.Json import zio.test.Assertion._ import zio.test.TestAspect._ diff --git a/zio-json/jvm/src/test/scala/zio/json/TestUtils.scala b/zio-json/jvm/src/test/scala/zio/json/TestUtils.scala index 498fc9cc2..191f221dc 100644 --- a/zio-json/jvm/src/test/scala/zio/json/TestUtils.scala +++ b/zio-json/jvm/src/test/scala/zio/json/TestUtils.scala @@ -1,4 +1,4 @@ -package testzio.json +package zio.json import zio._ import zio.stream._ diff --git a/zio-json/jvm/src/test/scala-2/zio/json/data/GeoJSON.scala b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala similarity index 68% rename from zio-json/jvm/src/test/scala-2/zio/json/data/GeoJSON.scala rename to zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala index 04f4605e6..cde606474 100644 --- a/zio-json/jvm/src/test/scala-2/zio/json/data/GeoJSON.scala +++ b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala @@ -1,19 +1,14 @@ -package testzio.json.data.geojson +package zio.json.data.geojson -import ai.x.play.json.Encoders.encoder -import ai.x.play.json.{ Jsonx => Playx } -import io.circe -import play.api.libs.{ json => Play } import zio.json._ import zio.json.ast._ - -object playtuples extends Play.GeneratedReads with Play.GeneratedWrites -import playtuples._ +import com.github.ghik.silencer.silent +import io.circe.{ Codec, Decoder, Encoder } +import io.circe.generic.semiauto.deriveCodec +import io.circe.syntax.EncoderOps package generated { - import com.github.ghik.silencer.silent - @jsonDiscriminator("type") sealed abstract class Geometry final case class Point(coordinates: (Double, Double)) extends Geometry @@ -40,24 +35,37 @@ package generated { implicit lazy val zioJsonEncoder: JsonEncoder[Geometry] = DeriveJsonEncoder.gen[Geometry] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - .copy(discriminator = Some("type")) - implicit lazy val circeJsonDecoder: circe.Decoder[Geometry] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Geometry] - implicit lazy val circeEncoder: circe.Encoder[Geometry] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Geometry] - - // it's not clear why this needs the extras package... - implicit val playPoint: Play.Format[Point] = Playx.formatCaseClass[Point] - implicit val playMultiPoint: Play.Format[MultiPoint] = Play.Json.format[MultiPoint] - implicit val playLineString: Play.Format[LineString] = Play.Json.format[LineString] - implicit val playMultiLineString: Play.Format[MultiLineString] = Play.Json.format[MultiLineString] - implicit val playPolygon: Play.Format[Polygon] = Play.Json.format[Polygon] - implicit val playMultiPolygon: Play.Format[MultiPolygon] = Play.Json.format[MultiPolygon] - implicit lazy val playGeometryCollection: Play.Format[GeometryCollection] = Play.Json.format[GeometryCollection] - implicit val playFormatter: Play.Format[Geometry] = Playx.formatSealed[Geometry] - + implicit lazy val circeJsonCodec: Codec[Geometry] = { + implicit val c1: Codec[Point] = deriveCodec + implicit val c2: Codec[MultiPoint] = deriveCodec + implicit val c3: Codec[LineString] = deriveCodec + implicit val c4: Codec[MultiLineString] = deriveCodec + implicit val c5: Codec[Polygon] = deriveCodec + implicit val c6: Codec[MultiPolygon] = deriveCodec + implicit val c8: Codec[GeometryCollection] = deriveCodec + Codec.from( + Decoder.instance(c => + c.downField("type").as[String].flatMap { + case "Point" => c.as[Point] + case "MultiPoint" => c.as[MultiPoint] + case "LineString" => c.as[LineString] + case "MultiLineString" => c.as[MultiLineString] + case "Polygon" => c.as[Polygon] + case "MultiPolygon" => c.as[MultiPolygon] + case "GeometryCollection" => c.as[GeometryCollection] + } + ), + Encoder.instance { + case x: Point => x.asJson.mapObject(_.+:("type" -> "Point".asJson)) + case x: MultiPoint => x.asJson.mapObject(_.+:("type" -> "MultiPoint".asJson)) + case x: LineString => x.asJson.mapObject(_.+:("type" -> "LineString".asJson)) + case x: MultiLineString => x.asJson.mapObject(_.+:("type" -> "MultiLineString".asJson)) + case x: Polygon => x.asJson.mapObject(_.+:("type" -> "Polygon".asJson)) + case x: MultiPolygon => x.asJson.mapObject(_.+:("type" -> "MultiPolygon".asJson)) + case x: GeometryCollection => x.asJson.mapObject(_.+:("type" -> "GeometryCollection".asJson)) + } + ) + } } @silent("Block result was adapted via implicit conversion") object GeoJSON { @@ -66,18 +74,22 @@ package generated { implicit lazy val zioJsonEncoder: JsonEncoder[GeoJSON] = DeriveJsonEncoder.gen[GeoJSON] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - .copy(discriminator = Some("type")) - implicit lazy val circeJsonDecoder: circe.Decoder[GeoJSON] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[GeoJSON] - implicit lazy val circeEncoder: circe.Encoder[GeoJSON] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[GeoJSON] - - implicit val playFeature: Play.Format[Feature] = Play.Json.format[Feature] - implicit lazy val playFeatureCollection: Play.Format[FeatureCollection] = Play.Json.format[FeatureCollection] - implicit val playFormatter: Play.Format[GeoJSON] = Playx.formatSealed[GeoJSON] - + implicit lazy val circeCodec: Codec[GeoJSON] = { + implicit val c1: Codec[Feature] = deriveCodec + implicit val c2: Codec[FeatureCollection] = deriveCodec + Codec.from( + Decoder.instance(c => + c.downField("type").as[String].flatMap { + case "Feature" => c.as[Feature] + case "FeatureCollection" => c.as[FeatureCollection] + } + ), + Encoder.instance { + case x: Feature => x.asJson.mapObject(_.+:("type" -> "Feature".asJson)) + case x: FeatureCollection => x.asJson.mapObject(_.+:("type" -> "FeatureCollection".asJson)) + } + ) + } } } @@ -114,7 +126,10 @@ package handrolled { // custom decoder (below) which is necessary to avert a DOS attack. implicit lazy val zioJsonJsonDecoder: JsonDecoder[Geometry] = new JsonDecoder[Geometry] { - import zio.json._, internal._, JsonDecoder.{ JsonError, UnsafeJson } + import zio.json._ + import JsonDecoder.{ JsonError, UnsafeJson } + import internal._ + import scala.annotation._ val names: Array[String] = Array("type", "coordinates", "geometries") @@ -186,7 +201,7 @@ package handrolled { var subtype: Int = -1 if (Lexer.firstField(trace, in)) - do { + while ({ val field = Lexer.field(trace, in, matrix) if (field == -1) Lexer.skipValue(trace, in) else { @@ -207,7 +222,8 @@ package handrolled { geometries = geometriesD.unsafeDecode(trace_, in) } } - } while (Lexer.nextField(trace, in)) + Lexer.nextField(trace, in) + }) () if (subtype == -1) throw UnsafeJson( @@ -240,23 +256,37 @@ package handrolled { } implicit lazy val zioJsonEncoder: JsonEncoder[Geometry] = DeriveJsonEncoder.gen[Geometry] - - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - .copy(discriminator = Some("type")) - implicit lazy val circeJsonDecoder: circe.Decoder[Geometry] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Geometry] - implicit lazy val circeEncoder: circe.Encoder[Geometry] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Geometry] - implicit val playPoint: Play.Format[Point] = Playx.formatCaseClass[Point] - implicit val playMultiPoint: Play.Format[MultiPoint] = Play.Json.format[MultiPoint] - implicit val playLineString: Play.Format[LineString] = Play.Json.format[LineString] - implicit val playMultiLineString: Play.Format[MultiLineString] = Play.Json.format[MultiLineString] - implicit val playPolygon: Play.Format[Polygon] = Play.Json.format[Polygon] - implicit val playMultiPolygon: Play.Format[MultiPolygon] = Play.Json.format[MultiPolygon] - implicit lazy val playGeometryCollection: Play.Format[GeometryCollection] = Play.Json.format[GeometryCollection] - implicit val playFormatter: Play.Format[Geometry] = Playx.formatSealed[Geometry] - + implicit lazy val circeJsonCodec: Codec[Geometry] = { + implicit val c1: Codec[Point] = deriveCodec + implicit val c2: Codec[MultiPoint] = deriveCodec + implicit val c3: Codec[LineString] = deriveCodec + implicit val c4: Codec[MultiLineString] = deriveCodec + implicit val c5: Codec[Polygon] = deriveCodec + implicit val c6: Codec[MultiPolygon] = deriveCodec + implicit val c8: Codec[GeometryCollection] = deriveCodec + Codec.from( + Decoder.instance(c => + c.downField("type").as[String].flatMap { + case "Point" => c.as[Point] + case "MultiPoint" => c.as[MultiPoint] + case "LineString" => c.as[LineString] + case "MultiLineString" => c.as[MultiLineString] + case "Polygon" => c.as[Polygon] + case "MultiPolygon" => c.as[MultiPolygon] + case "GeometryCollection" => c.as[GeometryCollection] + } + ), + Encoder.instance { + case x: Point => x.asJson.mapObject(_.+:("type" -> "Point".asJson)) + case x: MultiPoint => x.asJson.mapObject(_.+:("type" -> "MultiPoint".asJson)) + case x: LineString => x.asJson.mapObject(_.+:("type" -> "LineString".asJson)) + case x: MultiLineString => x.asJson.mapObject(_.+:("type" -> "MultiLineString".asJson)) + case x: Polygon => x.asJson.mapObject(_.+:("type" -> "Polygon".asJson)) + case x: MultiPolygon => x.asJson.mapObject(_.+:("type" -> "MultiPolygon".asJson)) + case x: GeometryCollection => x.asJson.mapObject(_.+:("type" -> "GeometryCollection".asJson)) + } + ) + } } @silent("Block result was adapted via implicit conversion") object GeoJSON { @@ -267,7 +297,10 @@ package handrolled { // of a corner case. implicit lazy val zioJsonJsonDecoder: JsonDecoder[GeoJSON] = new JsonDecoder[GeoJSON] { - import zio.json._, internal._, JsonDecoder.{ JsonError, UnsafeJson } + import zio.json._ + import JsonDecoder.{ JsonError, UnsafeJson } + import internal._ + import scala.annotation._ val names: Array[String] = @@ -292,7 +325,7 @@ package handrolled { var subtype: Int = -1 if (Lexer.firstField(trace, in)) - do { + while ({ val field = Lexer.field(trace, in, matrix) if (field == -1) Lexer.skipValue(trace, in) else { @@ -320,7 +353,8 @@ package handrolled { features = featuresD.unsafeDecode(trace_, in) } } - } while (Lexer.nextField(trace, in)) + Lexer.nextField(trace, in) + }) () if (subtype == -1) // we could infer the type but that would mean accepting invalid data @@ -351,19 +385,21 @@ package handrolled { } implicit lazy val zioJsonEncoder: JsonEncoder[GeoJSON] = DeriveJsonEncoder.gen[GeoJSON] - - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - .copy(discriminator = Some("type")) - implicit lazy val circeJsonDecoder: circe.Decoder[GeoJSON] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[GeoJSON] - implicit lazy val circeEncoder: circe.Encoder[GeoJSON] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[GeoJSON] - - implicit val playFeature: Play.Format[Feature] = Play.Json.format[Feature] - implicit lazy val playFeatureCollection: Play.Format[FeatureCollection] = Play.Json.format[FeatureCollection] - - implicit val playFormatter: Play.Format[GeoJSON] = Playx.formatSealed[GeoJSON] - + implicit lazy val circeCodec: Codec[GeoJSON] = { + implicit val c1: Codec[Feature] = deriveCodec + implicit val c2: Codec[FeatureCollection] = deriveCodec + Codec.from( + Decoder.instance(c => + c.downField("type").as[String].flatMap { + case "Feature" => c.as[Feature] + case "FeatureCollection" => c.as[FeatureCollection] + } + ), + Encoder.instance { + case x: Feature => x.asJson.mapObject(_.+:("type" -> "Feature".asJson)) + case x: FeatureCollection => x.asJson.mapObject(_.+:("type" -> "FeatureCollection".asJson)) + } + ) + } } } diff --git a/zio-json/jvm/src/test/scala/zio/json/data/googlemaps/GoogleMaps.scala b/zio-json/jvm/src/test/scala/zio/json/data/googlemaps/GoogleMaps.scala new file mode 100644 index 000000000..1447b2c74 --- /dev/null +++ b/zio-json/jvm/src/test/scala/zio/json/data/googlemaps/GoogleMaps.scala @@ -0,0 +1,54 @@ +package zio.json.data.googlemaps + +import com.github.ghik.silencer.silent +import com.github.plokhotnyuk.jsoniter_scala.macros.named +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec +import zio.json._ + +final case class Value( + text: String, + @named("value") + @jsonField("value") + value: Int +) +final case class Elements(distance: Value, duration: Value, status: String) +final case class Rows(elements: List[Elements]) +// @jsonNoExtraFields // entirely mitigates Attack1 +final case class DistanceMatrix( + destination_addresses: List[String], + origin_addresses: List[String], + rows: List[Rows], + status: String +) + +@silent("Block result was adapted via implicit conversion") +object Value { + implicit val zioJsonJsonDecoder: JsonDecoder[Value] = DeriveJsonDecoder.gen[Value] + implicit val zioJsonEncoder: JsonEncoder[Value] = DeriveJsonEncoder.gen[Value] + + implicit val circeCodec: Codec[Value] = deriveCodec +} +@silent("Block result was adapted via implicit conversion") +object Elements { + implicit val zioJsonJsonDecoder: JsonDecoder[Elements] = DeriveJsonDecoder.gen[Elements] + implicit val zioJsonEncoder: JsonEncoder[Elements] = DeriveJsonEncoder.gen[Elements] + + implicit val circeCodec: Codec[Elements] = deriveCodec +} +@silent("Block result was adapted via implicit conversion") +object Rows { + implicit val zioJsonJsonDecoder: JsonDecoder[Rows] = DeriveJsonDecoder.gen[Rows] + implicit val zioJsonEncoder: JsonEncoder[Rows] = DeriveJsonEncoder.gen[Rows] + + implicit val circeCodec: Codec[Rows] = deriveCodec +} +@silent("Block result was adapted via implicit conversion") +object DistanceMatrix { + implicit val zioJsonJsonDecoder: JsonDecoder[DistanceMatrix] = + DeriveJsonDecoder.gen[DistanceMatrix] + implicit val zioJsonEncoder: JsonEncoder[DistanceMatrix] = + DeriveJsonEncoder.gen[DistanceMatrix] + + implicit val circeCodec: Codec[DistanceMatrix] = deriveCodec +} diff --git a/zio-json/jvm/src/test/scala-2/zio/json/data/Twitter.scala b/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala similarity index 57% rename from zio-json/jvm/src/test/scala-2/zio/json/data/Twitter.scala rename to zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala index daee6afa5..3ec2e647c 100644 --- a/zio-json/jvm/src/test/scala-2/zio/json/data/Twitter.scala +++ b/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala @@ -1,10 +1,9 @@ -package testzio.json.data.twitter +package zio.json.data.twitter -import ai.x.play.json.Encoders.encoder -import ai.x.play.json.{ Jsonx => Playx } import com.github.ghik.silencer.silent import io.circe -import play.api.libs.{ json => Play } +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec import zio.json._ case class Urls( @@ -17,26 +16,16 @@ case class Urls( object Urls { implicit val jJsonDecoder: JsonDecoder[Urls] = DeriveJsonDecoder.gen[Urls] implicit val jEncoder: JsonEncoder[Urls] = DeriveJsonEncoder.gen[Urls] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[Urls] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Urls] - implicit val circeEncoder: circe.Encoder[Urls] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Urls] - implicit val playFormatter: Play.Format[Urls] = Play.Json.format[Urls] + + implicit val circeCodec: Codec[Urls] = deriveCodec } case class Url(urls: List[Urls]) @silent("Block result was adapted via implicit conversion") object Url { implicit val jJsonDecoder: JsonDecoder[Url] = DeriveJsonDecoder.gen[Url] implicit val jEncoder: JsonEncoder[Url] = DeriveJsonEncoder.gen[Url] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[Url] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Url] - implicit val circeEncoder: circe.Encoder[Url] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Url] - implicit val playFormatter: Play.Format[Url] = Play.Json.format[Url] + + implicit val circeCodec: Codec[Url] = deriveCodec } case class UserEntities(url: Url, description: Url) @@ -44,14 +33,8 @@ case class UserEntities(url: Url, description: Url) object UserEntities { implicit val jJsonDecoder: JsonDecoder[UserEntities] = DeriveJsonDecoder.gen[UserEntities] implicit val jEncoder: JsonEncoder[UserEntities] = DeriveJsonEncoder.gen[UserEntities] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[UserEntities] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[UserEntities] - implicit val circeEncoder: circe.Encoder[UserEntities] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[UserEntities] - implicit val playFormatter: Play.Format[UserEntities] = - Play.Json.format[UserEntities] + + implicit val circeCodec: Codec[UserEntities] = deriveCodec } case class UserMentions( @@ -65,14 +48,8 @@ case class UserMentions( object UserMentions { implicit val jJsonDecoder: JsonDecoder[UserMentions] = DeriveJsonDecoder.gen[UserMentions] implicit val jEncoder: JsonEncoder[UserMentions] = DeriveJsonEncoder.gen[UserMentions] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[UserMentions] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[UserMentions] - implicit val circeEncoder: circe.Encoder[UserMentions] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[UserMentions] - implicit val playFormatter: Play.Format[UserMentions] = - Play.Json.format[UserMentions] + + implicit val circeCodec: Codec[UserMentions] = deriveCodec } case class User( @@ -123,13 +100,8 @@ case class User( object User { implicit val jJsonDecoder: JsonDecoder[User] = DeriveJsonDecoder.gen[User] implicit val jEncoder: JsonEncoder[User] = DeriveJsonEncoder.gen[User] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[User] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[User] - implicit val circeEncoder: circe.Encoder[User] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[User] - implicit val playFormatter: Play.Format[User] = Playx.formatCaseClass[User] + + implicit val circeCodec: Codec[User] = deriveCodec } case class Entities( @@ -142,13 +114,8 @@ case class Entities( object Entities { implicit val jJsonDecoder: JsonDecoder[Entities] = DeriveJsonDecoder.gen[Entities] implicit val jEncoder: JsonEncoder[Entities] = DeriveJsonEncoder.gen[Entities] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[Entities] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Entities] - implicit val circeEncoder: circe.Encoder[Entities] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Entities] - implicit val playFormatter: Play.Format[Entities] = Play.Json.format[Entities] + + implicit val circeCodec: Codec[Entities] = deriveCodec } case class RetweetedStatus( @@ -183,14 +150,8 @@ object RetweetedStatus { DeriveJsonDecoder.gen[RetweetedStatus] implicit val jEncoder: JsonEncoder[RetweetedStatus] = DeriveJsonEncoder.gen[RetweetedStatus] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[RetweetedStatus] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[RetweetedStatus] - implicit val circeEncoder: circe.Encoder[RetweetedStatus] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[RetweetedStatus] - implicit val playFormatter: Play.Format[RetweetedStatus] = - Playx.formatCaseClass[RetweetedStatus] + + implicit val circeCodec: Codec[RetweetedStatus] = deriveCodec } case class Tweet( @@ -225,11 +186,6 @@ case class Tweet( object Tweet { implicit val zioJsonJsonDecoder: JsonDecoder[Tweet] = DeriveJsonDecoder.gen[Tweet] implicit val zioJsonEncoder: JsonEncoder[Tweet] = DeriveJsonEncoder.gen[Tweet] - implicit val customConfig: circe.generic.extras.Configuration = - circe.generic.extras.Configuration.default - implicit val circeJsonDecoder: circe.Decoder[Tweet] = - circe.generic.extras.semiauto.deriveConfiguredDecoder[Tweet] - implicit val circeEncoder: circe.Encoder[Tweet] = - circe.generic.extras.semiauto.deriveConfiguredEncoder[Tweet] - implicit val playFormatter: Play.Format[Tweet] = Playx.formatCaseClass[Tweet] + + implicit val circeCodec: Codec[Tweet] = deriveCodec } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala index 433042bd5..aafb70e4b 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala @@ -1,7 +1,6 @@ -package testzio.json +package zio.json import zio._ -import zio.json._ import zio.test.Assertion._ import zio.test._ diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala index d599c5b72..0426015b7 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala @@ -1,7 +1,6 @@ -package testzio.json +package zio.json import zio._ -import zio.json._ import zio.test.Assertion._ import zio.test._ diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala index 05e28dd6e..1c10fd299 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala @@ -1,7 +1,6 @@ -package testzio.json +package zio.json import zio._ -import zio.json._ import zio.test.Assertion._ import zio.test._ diff --git a/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala index e669376b3..429c93c1c 100644 --- a/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala @@ -1,6 +1,5 @@ package zio.json -import zio.json.JsonCodecConfiguration.SumTypeHandling.DiscriminatorField import zio.json.ast.Json import zio.test._ import zio.Chunk diff --git a/zio-json/jvm/src/test/scala/zio/json/CarterSpec.scala b/zio-json/shared/src/test/scala/zio/json/CarterSpec.scala similarity index 98% rename from zio-json/jvm/src/test/scala/zio/json/CarterSpec.scala rename to zio-json/shared/src/test/scala/zio/json/CarterSpec.scala index d19c899e1..99283326b 100644 --- a/zio-json/jvm/src/test/scala/zio/json/CarterSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CarterSpec.scala @@ -1,6 +1,5 @@ -package testzio.json +package zio.json -import zio.json._ import zio.test.Assertion._ import zio.test._ diff --git a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala index 468017ecc..13a295ff4 100644 --- a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala @@ -1,13 +1,11 @@ -package testzio.json +package zio.json import zio._ -import zio.json._ import zio.json.ast.Json import zio.test.Assertion._ import zio.test.TestAspect.jvmOnly import zio.test._ -import java.math.BigInteger import scala.collection.immutable object CodecSpec extends ZIOSpecDefault { diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index fd7088738..55eb0d051 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -1,7 +1,6 @@ -package testzio.json +package zio.json import zio._ -import zio.json._ import zio.json.ast.Json import zio.test.Assertion._ import zio.test.TestAspect.jvmOnly @@ -98,10 +97,10 @@ object DecoderSpec extends ZIOSpecDefault { DeriveJsonDecoder.gen[Mango] }.flip } yield assertTrue( - // Class name in Scala 2: testzio.json.DecoderSpec.spec.Mango - // Class name in Scala 3: testzio.json.DecoderSpec.spec.$anonfun.Mango + // Class name in Scala 2: zio.json.DecoderSpec.spec.Mango + // Class name in Scala 3: zio.json.DecoderSpec.spec.$anonfun.Mango error.getMessage.matches( - "Field names and aliases in case class testzio.json.DecoderSpec.spec(.\\$anonfun)?.Mango must be distinct, alias\\(es\\) r collide with a field or another alias" + "Field names and aliases in case class zio.json.DecoderSpec.spec(.\\$anonfun)?.Mango must be distinct, alias\\(es\\) r collide with a field or another alias" ) ) }, @@ -113,7 +112,7 @@ object DecoderSpec extends ZIOSpecDefault { }.flip } yield assertTrue( error.getMessage.matches( - "Field names and aliases in case class testzio.json.DecoderSpec.spec(.\\$anonfun)?.Mango must be distinct, alias\\(es\\) r collide with a field or another alias" + "Field names and aliases in case class zio.json.DecoderSpec.spec(.\\$anonfun)?.Mango must be distinct, alias\\(es\\) r collide with a field or another alias" ) ) }, @@ -125,7 +124,7 @@ object DecoderSpec extends ZIOSpecDefault { }.flip } yield assertTrue( error.getMessage.matches( - "Field names and aliases in case class testzio.json.DecoderSpec.spec(.\\$anonfun)?.Mango must be distinct, alias\\(es\\) r collide with a field or another alias" + "Field names and aliases in case class zio.json.DecoderSpec.spec(.\\$anonfun)?.Mango must be distinct, alias\\(es\\) r collide with a field or another alias" ) ) }, diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index 914099e64..31309cb31 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -1,6 +1,5 @@ -package testzio.json +package zio.json -import zio.json._ import zio.json.ast.Json import zio.test.Assertion._ import zio.test.TestAspect.jvmOnly @@ -10,7 +9,7 @@ import zio.{ Chunk, NonEmptyChunk } import java.util.UUID import scala.collection.{ immutable, mutable } -// zioJsonJVM/testOnly testzio.json.EncoderSpec +// zioJsonJVM/testOnly zio.json.EncoderSpec object EncoderSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = diff --git a/zio-json/shared/src/test/scala/zio/json/Gens.scala b/zio-json/shared/src/test/scala/zio/json/Gens.scala index 354140c64..90e8e0f36 100644 --- a/zio-json/shared/src/test/scala/zio/json/Gens.scala +++ b/zio-json/shared/src/test/scala/zio/json/Gens.scala @@ -1,4 +1,4 @@ -package testzio.json +package zio.json import zio.test.Gen diff --git a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala index 7684e8187..3ef3584a0 100644 --- a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala @@ -1,13 +1,12 @@ -package testzio.json +package zio.json -import zio.json._ import zio.test.Assertion._ import zio.test._ import java.time._ import java.time.format.DateTimeFormatter -// zioJsonJVM/testOnly testzio.json.JavaTimeSpec +// zioJsonJVM/testOnly zio.json.JavaTimeSpec object JavaTimeSpec extends ZIOSpecDefault { private def stringify(s: Any): String = s""" "${s.toString}" """ diff --git a/zio-json/shared/src/test/scala/zio/json/RoundTripSpec.scala b/zio-json/shared/src/test/scala/zio/json/RoundTripSpec.scala index e13b653c6..dcca2473e 100644 --- a/zio-json/shared/src/test/scala/zio/json/RoundTripSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/RoundTripSpec.scala @@ -1,7 +1,6 @@ -package testzio.json +package zio.json -import testzio.json.Gens._ -import zio.json._ +import zio.json.Gens._ import zio.json.ast.Json import zio.test.Assertion._ import zio.test.TestAspect._ diff --git a/zio-json/jvm/src/test/scala/zio/json/internal/SafeNumbersSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala similarity index 98% rename from zio-json/jvm/src/test/scala/zio/json/internal/SafeNumbersSpec.scala rename to zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala index 41f184676..c5c91525d 100644 --- a/zio-json/jvm/src/test/scala/zio/json/internal/SafeNumbersSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala @@ -1,8 +1,8 @@ -package testzio.json.internal +package zio.json.internal -import testzio.json.Gens._ -import zio.json.internal._ +import zio.json.Gens._ import zio.test.Assertion._ +import zio.test.TestAspect.jvmOnly import zio.test._ object SafeNumbersSpec extends ZIOSpecDefault { @@ -153,7 +153,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { test("large mantissa") { // https://github.com/zio/zio-json/issues/221 assert(SafeNumbers.float("1.199999988079071"))(equalTo(FloatSome(1.1999999f))) - }, + } @@ jvmOnly, test("valid (from Int)") { check(Gen.int)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.toFloat)))) }, diff --git a/zio-json/jvm/src/test/scala/zio/json/internal/StringMatrixSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala similarity index 62% rename from zio-json/jvm/src/test/scala/zio/json/internal/StringMatrixSpec.scala rename to zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala index 1dd5f2992..ef0c40568 100644 --- a/zio-json/jvm/src/test/scala/zio/json/internal/StringMatrixSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala @@ -1,48 +1,50 @@ -package testzio.json.internal +package zio.json.internal -import zio.json.internal._ import zio.test.Assertion._ +import zio.test.TestAspect._ import zio.test._ object StringMatrixSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = suite("StringMatrix")( test("basic positive succeeds") { - val names = List("\uD83D\uDE00" /* a surrogate pair for the grinning face */, "a", "b") - val aliases = List("c" -> 0, "d" -> 1) + val names = Array("\uD83D\uDE00" /* a surrogate pair for the grinning face */, "a", "b") + val aliases = Array("c" -> 0, "d" -> 1) val asserts = - names.map(s => matcher(names, aliases, s).contains(s)) ++ - aliases.map(a => matcher(names, aliases, a._1).contains(a._1)) + (names.map(s => matcher(names, aliases, s).contains(s)) ++ + aliases.map(a => matcher(names, aliases, a._1).contains(a._1))).toVector assert(asserts)(forall(isTrue)) }, test("positive succeeds") { // Watch out: TestStrings were passed check(genTestStrings) { xs => - val asserts = xs.map(s => matcher(xs, List.empty, s).contains(s)) + val asserts = xs.map(s => matcher(xs, Array.empty, s).contains(s)).toVector assert(asserts)(forall(isTrue)) } }, test("negative fails") { - check(genTestStrings.filterNot(_.startsWith("wibble")))(xs => assert(matcher(xs, List.empty, "wibble"))(isEmpty)) + check(genTestStrings.filterNot(_.startsWith("wibble")))(xs => + assert(matcher(xs, Array.empty, "wibble").toVector)(isEmpty) + ) }, test("substring fails") { - check(genTestStrings.filter(_.length > 1))(xs => assert(matcher(xs, List.empty, xs.mkString))(isEmpty)) + check(genTestStrings.filter(_.length > 1))(xs => assert(matcher(xs, Array.empty, xs.mkString).toVector)(isEmpty)) }, test("trivial") { - check(genNonEmptyString)(s => assert(matcher(List(s), List.empty, s))(equalTo(List(s)))) + check(genNonEmptyString)(s => assert(matcher(Array(s), Array.empty, s).toVector)(equalTo(Vector(s)))) }, test("exact match is a substring") { assert( matcher( - List("retweeted_status", "retweeted"), - List.empty, + Array("retweeted_status", "retweeted"), + Array.empty, "retweeted" - ) - )(equalTo(List("retweeted"))) + ).toVector + )(equalTo(Vector("retweeted"))) }, test("first resolves to field index") { check(genTestStrings) { xs => - val m = new StringMatrix(xs.toArray) + val m = new StringMatrix(xs) val asserts = xs.indices.map { i => val test = xs(i) var bs = test.zipWithIndex.foldLeft(m.initial) { case (bs, (c, i)) => @@ -58,8 +60,8 @@ object StringMatrixSpec extends ZIOSpecDefault { // Watch out: TestStrings were passed check(genTestStringsAndAliases) { case (xs, aliases) => val asserts = - xs.map(s => matcher(xs, List.empty, s).contains(s)) ++ - aliases.map(alias => matcher(xs, aliases, alias._1).contains(alias._1)) + (xs.map(s => matcher(xs, Array.empty, s).contains(s)) ++ + aliases.map(alias => matcher(xs, aliases, alias._1).contains(alias._1))).toVector assert(asserts)(forall(isTrue)) } @@ -70,33 +72,33 @@ object StringMatrixSpec extends ZIOSpecDefault { xs.exists(_.startsWith("wibble")) || aliases.exists(_._1.startsWith("wibble")) } ) { case (xs, aliases) => - assert(matcher(xs, aliases, "wibble"))(isEmpty) + assert(matcher(xs, aliases, "wibble").toVector)(isEmpty) } }, test("alias substring fails") { check( genTestStringsAndAliases.filter { case (xs, aliases) => xs.length + aliases.length > 1 } ) { case (xs, aliases) => - assert(matcher(xs, aliases, xs.mkString + aliases.map(_._1).mkString))(isEmpty) + assert(matcher(xs, aliases, xs.mkString + aliases.map(_._1).mkString).toVector)(isEmpty) } }, test("alias trivial") { check(genNonEmptyString.filterNot(_.startsWith("wibble")))(s => - assert(matcher(List("wibble"), List(s -> 0), s))(equalTo(List(s))) + assert(matcher(Array("wibble"), Array(s -> 0), s).toVector)(equalTo(Vector(s))) ) }, test("alias exact match is a substring") { assert( matcher( - List("wibble"), - List("retweeted_status" -> 0, "retweeted" -> 0), + Array("wibble"), + Array("retweeted_status" -> 0, "retweeted" -> 0), "retweeted" - ) - )(equalTo(List("retweeted"))) + ).toVector + )(equalTo(Vector("retweeted"))) }, test("alias first resolves to aliased field index") { check(genTestStringsAndAliases) { case (xs, aliases) => - val m = new StringMatrix(xs.toArray, aliases.toArray) + val m = new StringMatrix(xs, aliases) val asserts = aliases.indices.map { i => val test = aliases(i)._1 var bs = test.zipWithIndex.foldLeft(m.initial) { case (bs, (c, i)) => @@ -108,7 +110,7 @@ object StringMatrixSpec extends ZIOSpecDefault { assert(asserts)(forall(isTrue)) } } - ) + ) @@ jvm(samples(100)) @@ js(samples(10)) @@ native(samples(10)) val genNonEmptyString = Gen.alphaNumericString.filter(_.nonEmpty) @@ -117,34 +119,37 @@ object StringMatrixSpec extends ZIOSpecDefault { for { n <- Gen.int(1, 64) xs <- Gen.setOfN(n)(genNonEmptyString) - } yield xs.toList + } yield xs.toArray val genTestStringsAndAliases = for { xsn <- Gen.int(1, 64) xs <- Gen.setOfN(xsn)(genNonEmptyString) an <- Gen.int(0, 64 - xsn) - aliasF <- Gen.setOfN(an)(genNonEmptyString.filter(a => !xs.contains(a))).map(_.toList) - aliasN <- Gen.listOfN(an)(Gen.int(0, xsn - 1)) - } yield (xs.toList, aliasF zip aliasN) + aliasF <- Gen.setOfN(an)(genNonEmptyString.filter(a => !xs.contains(a))).map(_.toArray) + aliasN <- Gen.listOfN(an)(Gen.int(0, xsn - 1)).map(_.toArray) + } yield (xs.toArray, aliasF zip aliasN) - private def matcher(xs: List[String], aliases: List[(String, Int)], test: String): List[String] = { - val m = new StringMatrix(xs.toArray, aliases.toArray) - var bs = test.zipWithIndex.foldLeft(m.initial) { case (bs, (c, i)) => - m.update(bs, i, c.toInt) + private def matcher(xs: Array[String], aliases: Array[(String, Int)], test: String): Array[String] = { + val m = new StringMatrix(xs, aliases) + var bs = test.foldLeft(m.initial) { + var i = 0 + (bs, c) => + val nm = m.update(bs, i, c.toInt) + i += 1 + nm } bs = m.exact(bs, test.length) matches(xs ++ aliases.map(_._1), bs) } - private def matches(xsAndAliases: List[String], bitset: Long): List[String] = { - var hits: List[String] = Nil - var i = 0 + private def matches(xsAndAliases: Array[String], bitset: Long): Array[String] = { + val hits = Array.newBuilder[String] + var i = 0 while (i < xsAndAliases.length) { - if (((bitset >>> i) & 1L) == 1L) - hits = xsAndAliases(i) :: hits + if (((bitset >>> i) & 1L) != 0) hits += xsAndAliases(i) i += 1 } - hits + hits.result() } } From 19775e47ba01c744413fd325bceb4e8647d54227 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 17 Jan 2025 08:47:29 +0100 Subject: [PATCH 072/311] Faster parsing of JSON keys and values (#1221) * Reduce memory footprint and CPU overhead for `StringMatrix` * Extract error throwing and inline string parsing --- .../json/DecoderPlatformSpecificSpec.scala | 2 +- .../main/scala/zio/json/internal/lexer.scala | 371 ++++++++++-------- .../scala/zio/json/internal/readers.scala | 12 +- .../scala/zio/json/internal/writers.scala | 4 +- .../zio/json/internal/StringMatrixSpec.scala | 6 +- 5 files changed, 212 insertions(+), 183 deletions(-) diff --git a/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala b/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala index bc6a53a32..10cfacfae 100644 --- a/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala +++ b/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala @@ -266,7 +266,7 @@ object DecoderPlatformSpecificSpec extends ZIOSpecDefault { } assert(decoder.decodeJson("true"))(equalTo(Right(true.asInstanceOf[AnyVal]))) && assert(decoder.decodeJson("42"))(equalTo(Right(42.asInstanceOf[AnyVal]))) && - assert(decoder.decodeJson("\"a string\""))(equalTo(Left("(expected a number, got a)"))) + assert(decoder.decodeJson("\"a string\""))(equalTo(Left("(expected a number, got 'a')"))) } ) ) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index ee9ed00cc..984363e78 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -28,6 +28,18 @@ object Lexer { val NumberMaxBits: Int = 256 + @noinline + def error(msg: String, trace: List[JsonError]): Nothing = + throw UnsafeJson(JsonError.Message(msg) :: trace) + + @noinline + private[json] def error(expected: String, got: Char, trace: List[JsonError]): Nothing = + throw UnsafeJson(JsonError.Message(s"expected $expected got '$got'") :: trace) + + @noinline + private[json] def error(c: Char, trace: List[JsonError]): Nothing = + error(s"invalid '\\$c' in string", trace) + // True if we got a string (implies a retraction), False for } def firstField(trace: List[JsonError], in: RetractReader): Boolean = (in.nextNonWhitespace(): @switch) match { @@ -35,10 +47,7 @@ object Lexer { in.retract() true case '}' => false - case c => - throw UnsafeJson( - JsonError.Message(s"expected string or '}' got '$c'") :: trace - ) + case c => error("string or '}'", c, trace) } // True if we got a comma, and False for } @@ -46,10 +55,7 @@ object Lexer { (in.nextNonWhitespace(): @switch) match { case ',' => true case '}' => false - case c => - throw UnsafeJson( - JsonError.Message(s"expected ',' or '}' got '$c'") :: trace - ) + case c => error("',' or '}'", c, trace) } // True if we got anything besides a ], False for ] @@ -65,10 +71,7 @@ object Lexer { (in.nextNonWhitespace(): @switch) match { case ',' => true case ']' => false - case c => - throw UnsafeJson( - JsonError.Message(s"expected ',' or ']' got '$c'") :: trace - ) + case c => error("',' or ']'", c, trace) } // avoids allocating lots of strings (they are often the bulk of incoming @@ -90,12 +93,28 @@ object Lexer { in: OneCharReader, matrix: StringMatrix ): Int = { - val stream = streamingString(trace, in) - - var i: Int = 0 - var bs: Long = matrix.initial - var c: Int = -1 - while ({ c = stream.read(); c != -1 }) { + var c = in.nextNonWhitespace() + if (c != '"') error("'\"'", c, trace) + var bs = matrix.initial + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') { + (in.readChar(): @switch) match { + case '"' => c = '"' + case '\\' => c = '\\' + case '/' => c = '/' + case 'b' => c = '\b' + case 'f' => c = '\f' + case 'n' => c = '\n' + case 'r' => c = '\r' + case 't' => c = '\t' + case 'u' => c = nextHex4(trace, in) + case _ => error(c, trace) + } + } else if (c < ' ') error("invalid control in string", trace) bs = matrix.update(bs, i, c) i += 1 } @@ -103,9 +122,6 @@ object Lexer { matrix.first(bs) } - private[this] val alse: Array[Char] = "alse".toCharArray - private[this] val rue: Array[Char] = "rue".toCharArray - def skipValue(trace: List[JsonError], in: RetractReader): Unit = (in.nextNonWhitespace(): @switch) match { case 'n' | 't' => skipFixedChars(in, 3) @@ -116,7 +132,7 @@ object Lexer { skipString(in, evenBackSlashes = true) case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => skipNumber(in) - case c => throw UnsafeJson(JsonError.Message(s"unexpected '$c'") :: trace) + case c => error(s"unexpected '$c'", trace) } def skipNumber(in: RetractReader): Unit = { @@ -169,36 +185,104 @@ object Lexer { in: OneCharReader ): java.io.Reader = { char(trace, in, '"') - new EscapedString(trace, in) + new OneCharReader { + def close(): Unit = in.close() + + private[this] var escaped = false + + @tailrec + override def read(): Int = { + val c = in.readChar() + if (escaped) { + escaped = false + ((c: @switch) match { + case '"' | '\\' | '/' => c + case 'b' => '\b' + case 'f' => '\f' + case 'n' => '\n' + case 'r' => '\r' + case 't' => '\t' + case 'u' => Lexer.nextHex4(trace, in) + case _ => Lexer.error(c, trace) + }).toInt + } else if (c == '\\') { + escaped = true + read() + } else if (c == '"') -1 // this is the EOS for the caller + else if (c < ' ') Lexer.error("invalid control in string", trace) + else c.toInt + } + + // callers expect to get an EOB so this is rare + def readChar(): Char = { + val v = read() + if (v == -1) throw new UnexpectedEnd + v.toChar + } + } } def string(trace: List[JsonError], in: OneCharReader): CharSequence = { - char(trace, in, '"') - val stream = new EscapedString(trace, in) - + var c = in.nextNonWhitespace() + if (c != '"') error("'\"'", c, trace) val sb = new FastStringBuilder(64) - while (true) { - val c = stream.read() - if (c == -1) - return sb.buffer // mutable thing escapes, but cannot be changed - sb.append(c.toChar) + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') { + (in.readChar(): @switch) match { + case '"' => c = '"' + case '\\' => c = '\\' + case '/' => c = '/' + case 'b' => c = '\b' + case 'f' => c = '\f' + case 'n' => c = '\n' + case 'r' => c = '\r' + case 't' => c = '\t' + case 'u' => c = nextHex4(trace, in) + case _ => error(c, trace) + } + } else if (c < ' ') error("invalid control in string", trace) + sb.append(c) } - throw UnsafeJson(JsonError.Message("impossible string") :: trace) + sb.buffer } - def boolean(trace: List[JsonError], in: OneCharReader): Boolean = - (in.nextNonWhitespace(): @switch) match { + // consumes 4 hex characters after current + @noinline + def nextHex4(trace: List[JsonError], in: OneCharReader): Char = { + var i, accum = 0 + while (i < 4) { + val c = in.readChar() + accum <<= 4 + accum += { + if ('0' <= c && c <= '9') c - '0' + else if ('A' <= c && c <= 'F') c - 'A' + 10 + else if ('a' <= c && c <= 'f') c - 'a' + 10 + else error("invalid charcode in string", trace) + } + i += 1 + } + accum.toChar + } + + def boolean(trace: List[JsonError], in: OneCharReader): Boolean = { + val c1 = in.nextNonWhitespace() + val c2 = in.readChar() + val c3 = in.readChar() + val c4 = in.readChar() + (c1: @switch) match { case 't' => - readChars(trace, in, rue, "true") + if (c2 != 'r' || c3 != 'u' || c4 != 'e') error(s"expected 'true'", trace) true case 'f' => - readChars(trace, in, alse, "false") + if (in.readChar() != 'e' || c2 != 'a' || c3 != 'l' || c4 != 's') error(s"expected 'false'", trace) false case c => - throw UnsafeJson( - JsonError.Message(s"expected 'true' or 'false' got $c") :: trace - ) + error("'true' or 'false'", c, trace) } + } def byte(trace: List[JsonError], in: RetractReader): Byte = { checkNumber(trace, in) @@ -207,8 +291,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => - throw UnsafeJson(JsonError.Message("expected a Byte") :: trace) + case UnsafeNumbers.UnsafeNumber => error("expected a Byte", trace) } } @@ -219,8 +302,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => - throw UnsafeJson(JsonError.Message("expected a Short") :: trace) + case UnsafeNumbers.UnsafeNumber => error("expected a Short", trace) } } @@ -231,8 +313,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => - throw UnsafeJson(JsonError.Message("expected an Int") :: trace) + case UnsafeNumbers.UnsafeNumber => error("expected an Int", trace) } } @@ -243,8 +324,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => - throw UnsafeJson(JsonError.Message("expected a Long") :: trace) + case UnsafeNumbers.UnsafeNumber => error("expected a Long", trace) } } @@ -258,8 +338,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => - throw UnsafeJson(JsonError.Message(s"expected a $NumberMaxBits bit BigInteger") :: trace) + case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits bit BigInteger", trace) } } @@ -270,8 +349,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => - throw UnsafeJson(JsonError.Message("expected a Float") :: trace) + case UnsafeNumbers.UnsafeNumber => error("expected a Float", trace) } } @@ -282,8 +360,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => - throw UnsafeJson(JsonError.Message("expected a Double") :: trace) + case UnsafeNumbers.UnsafeNumber => error("expected a Double", trace) } } @@ -297,8 +374,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => - throw UnsafeJson(JsonError.Message(s"expected a $NumberMaxBits BigDecimal") :: trace) + case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits BigDecimal", trace) } } @@ -306,10 +382,7 @@ object Lexer { private def checkNumber(trace: List[JsonError], in: RetractReader): Unit = { (in.nextNonWhitespace(): @switch) match { case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => () - case c => - throw UnsafeJson( - JsonError.Message(s"expected a number, got $c") :: trace - ) + case c => error("a number,", c, trace) } in.retract() } @@ -317,8 +390,7 @@ object Lexer { // optional whitespace and then an expected character @inline def char(trace: List[JsonError], in: OneCharReader, c: Char): Unit = { val got = in.nextNonWhitespace() - if (got != c) - throw UnsafeJson(JsonError.Message(s"expected '$c' got '$got'") :: trace) + if (got != c) error(s"'$c'", got, trace) } @inline def charOnly( @@ -327,8 +399,7 @@ object Lexer { c: Char ): Unit = { val got = in.readChar() - if (got != c) - throw UnsafeJson(JsonError.Message(s"expected '$c' got '$got'") :: trace) + if (got != c) error(s"'$c'", got, trace) } // non-positional for performance @@ -347,133 +418,92 @@ object Lexer { ): Unit = { var i: Int = 0 while (i < expect.length) { - if (in.readChar() != expect(i)) - throw UnsafeJson(JsonError.Message(s"expected '$errMsg'") :: trace) - i += 1 - } - } - -} - -// A Reader for the contents of a string, taking care of the escaping. -// -// `read` can throw extra exceptions on badly formed input. -private final class EscapedString(trace: List[JsonError], in: OneCharReader) extends java.io.Reader with OneCharReader { - - def close(): Unit = in.close() - - private[this] var escaped = false - - override def read(): Int = { - val c = in.readChar() - if (escaped) { - escaped = false - (c: @switch) match { - case '"' | '\\' | '/' => c.toInt - case 'b' => '\b'.toInt - case 'f' => '\f'.toInt - case 'n' => '\n'.toInt - case 'r' => '\r'.toInt - case 't' => '\t'.toInt - case 'u' => nextHex4() - case _ => - throw UnsafeJson( - JsonError.Message(s"invalid '\\${c.toChar}' in string") :: trace - ) - } - } else if (c == '\\') { - escaped = true - read() - } else if (c == '"') -1 // this is the EOS for the caller - else if (c < ' ') - throw UnsafeJson(JsonError.Message("invalid control in string") :: trace) - else c.toInt - } - - // callers expect to get an EOB so this is rare - def readChar(): Char = { - val v = read() - if (v == -1) throw new UnexpectedEnd - v.toChar - } - - // consumes 4 hex characters after current - def nextHex4(): Int = { - var i: Int = 0 - var accum: Int = 0 - while (i < 4) { - var c: Int = in.read() - if (c == -1) - throw UnsafeJson(JsonError.Message("unexpected EOB in string") :: trace) - c = - if ('0' <= c && c <= '9') c - '0' - else if ('A' <= c && c <= 'F') c - 'A' + 10 - else if ('a' <= c && c <= 'f') c - 'a' + 10 - else - throw UnsafeJson( - JsonError.Message("invalid charcode in string") :: trace - ) - accum = accum * 16 + c + if (in.readChar() != expect(i)) error(s"expected '$errMsg'", trace) i += 1 } - accum } - } // A data structure encoding a simple algorithm for Trie pruning: Given a list // of strings, and a sequence of incoming characters, find the strings that // match, by manually maintaining a bitset. Empty strings are not allowed. -final class StringMatrix(val xs: Array[String], aliases: Array[(String, Int)] = Array.empty) { - require(xs.forall(_.nonEmpty)) +final class StringMatrix(xs: Array[String], aliases: Array[(String, Int)] = Array.empty) { require(xs.nonEmpty) - require(aliases.forall(_._1.nonEmpty)) - require(aliases.forall(p => p._2 >= 0 && p._2 < xs.length)) - val width: Int = xs.length + aliases.length + private[this] val width: Int = xs.length + aliases.length require(width <= 64) - val lengths: Array[Int] = Array.tabulate[Int](width) { string => - if (string < xs.length) xs(string).length - else aliases(string - xs.length)._1.length - } - val height: Int = lengths.max val initial: Long = -1L >>> (64 - width) - private val matrix: Array[Char] = { - val m = Array.fill[Char](width * height)(0xffff) + + private[this] val lengths: Array[Int] = { + val ls = new Array[Int](width) + val xsLen = xs.length + var string = 0 + while (string < xsLen) { + val l = xs(string).length + if (l == 0) require(false) + ls(string) = l + string += 1 + } + while (string < ls.length) { + val l = aliases(string - xsLen)._1.length + if (l == 0) require(false) + ls(string) = l + string += 1 + } + ls + } + private[this] val height: Int = lengths.max + private[this] val matrix: Array[Char] = { + val w = width + val m = new Array[Char](height * w) + val xsLen = xs.length var string = 0 - while (string < width) { + while (string < w) { val s = - if (string < xs.length) xs(string) - else aliases(string - xs.length)._1 - val len = s.length - var char = 0 + if (string < xsLen) xs(string) + else aliases(string - xsLen)._1 + val len = s.length + var char, base = 0 while (char < len) { - m(width * char + string) = s.charAt(char) + m(base + string) = s.charAt(char) + base += w char += 1 } string += 1 } m } - private val resolve: Array[Byte] = Array.tabulate[Byte](width) { string => - if (string < xs.length) string.toByte - else aliases(string - xs.length)._2.toByte + private[this] val resolvers: Array[Byte] = { + val rs = new Array[Byte](width) + val xsLen = xs.length + var string = 0 + while (string < xsLen) { + rs(string) = string.toByte + string += 1 + } + while (string < rs.length) { + val x = aliases(string - xsLen)._2 + if (x < 0 || x > xsLen) require(false) + rs(string) = x.toByte + string += 1 + } + rs } // must be called with increasing `char` (starting with bitset obtained from a // call to 'initial', char = 0) - def update(bitset: Long, char: Int, c: Int): Long = - if (char >= height) 0L // too long - else if (bitset == 0L) 0L // everybody lost - else { + def update(bitset: Long, char: Int, c: Char): Long = + if (char < height) { + val w = width + val m = matrix + val base = char * w var latest = bitset - val base = width * char - if (bitset == initial) { // special case when it is dense since it is simple + if (initial == bitset) { // special case when it is dense since it is simple var string = 0 - while (string < width) { - if (matrix(base + string) != c) latest ^= 1L << string + while (string < w) { + if (m(base + string) != c) latest ^= 1L << string string += 1 } } else { @@ -481,29 +511,28 @@ final class StringMatrix(val xs: Array[String], aliases: Array[(String, Int)] = while (remaining != 0L) { val string = java.lang.Long.numberOfTrailingZeros(remaining) val bit = 1L << string - if (matrix(base + string) != c) latest ^= bit remaining ^= bit + if (m(base + string) != c) latest ^= bit } } latest - } + } else 0L // too long // excludes entries that are not the given exact length def exact(bitset: Long, length: Int): Long = - if (length > height) 0L // too long - else { - var latest = bitset - var remaining = bitset + if (length <= height) { + var remaining, latest = bitset + val ls = lengths while (remaining != 0L) { val string = java.lang.Long.numberOfTrailingZeros(remaining) val bit = 1L << string - if (lengths(string) != length) latest ^= bit remaining ^= bit + if (ls(string) != length) latest ^= bit } latest - } + } else 0L // too long def first(bitset: Long): Int = - if (bitset == 0L) -1 - else resolve(java.lang.Long.numberOfTrailingZeros(bitset)) // never returns 64 + if (bitset != 0L) resolvers(java.lang.Long.numberOfTrailingZeros(bitset)).toInt // never returns 64 + else -1 } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala index 95a715b8f..eece2aa09 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala @@ -101,21 +101,21 @@ private[zio] final class FastStringReader(s: CharSequence) extends RetractReader override def read(): Int = { i += 1 if (i > len) -1 - else history(i - 1).toInt // -1 is faster than assigning a temp value + else s.charAt(i - 1).toInt // -1 is faster than assigning a temp value } override def readChar(): Char = { i += 1 if (i > len) throw new UnexpectedEnd - else history(i - 1) + s.charAt(i - 1) } override def nextNonWhitespace(): Char = { while ({ { i += 1 if (i > len) throw new UnexpectedEnd - }; isWhitespace(history(i - 1)) + }; isWhitespace(s.charAt(i - 1)) }) () - history(i - 1) + s.charAt(i - 1) } def retract(): Unit = i -= 1 @@ -179,7 +179,7 @@ private[zio] sealed trait PlaybackReader extends OneCharReader { private[zio] final class WithRecordingReader(in: OneCharReader, initial: Int) extends RecordingReader with PlaybackReader { - private[this] var tape: Array[Char] = Array.ofDim(Math.max(initial, 1)) + private[this] var tape: Array[Char] = new Array(Math.max(initial, 1)) private[this] var eob: Int = -1 private[this] var writing: Int = 0 private[this] var reading: Int = -1 @@ -209,7 +209,7 @@ private[zio] final class WithRecordingReader(in: OneCharReader, initial: Int) tape(writing) = v writing += 1 if (writing == tape.length) - tape = Arrays.copyOf(tape, tape.length * 2) + tape = Arrays.copyOf(tape, tape.length << 1) } v } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/writers.scala b/zio-json/shared/src/main/scala/zio/json/internal/writers.scala index 482e384c7..dff590e2c 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/writers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/writers.scala @@ -48,9 +48,9 @@ private[zio] final class FastStringBuilder(initial: Int) { private[this] var chars: Array[Char] = new Array[Char](initial) private[this] var i: Int = 0 + @inline def append(c: Char): Unit = { - if (i == chars.length) - chars = Arrays.copyOf(chars, chars.length * 2) + if (i == chars.length) chars = Arrays.copyOf(chars, chars.length << 1) chars(i) = c i += 1 } diff --git a/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala index ef0c40568..b3ce953a5 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala @@ -48,7 +48,7 @@ object StringMatrixSpec extends ZIOSpecDefault { val asserts = xs.indices.map { i => val test = xs(i) var bs = test.zipWithIndex.foldLeft(m.initial) { case (bs, (c, i)) => - m.update(bs, i, c.toInt) + m.update(bs, i, c) } bs = m.exact(bs, test.length) m.first(bs) == i @@ -102,7 +102,7 @@ object StringMatrixSpec extends ZIOSpecDefault { val asserts = aliases.indices.map { i => val test = aliases(i)._1 var bs = test.zipWithIndex.foldLeft(m.initial) { case (bs, (c, i)) => - m.update(bs, i, c.toInt) + m.update(bs, i, c) } bs = m.exact(bs, test.length) m.first(bs) == aliases(i)._2 @@ -135,7 +135,7 @@ object StringMatrixSpec extends ZIOSpecDefault { var bs = test.foldLeft(m.initial) { var i = 0 (bs, c) => - val nm = m.update(bs, i, c.toInt) + val nm = m.update(bs, i, c) i += 1 nm } From b3759cda7532816330a367b996ee94ddadb3ecd4 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 17 Jan 2025 20:05:28 +0100 Subject: [PATCH 073/311] Fix backward binary compatibility with v0.7.3 (#1222) --- project/BuildHelper.scala | 7 ++- .../src/main/scala-3/zio/json/macros.scala | 14 +++-- .../zio/json/JsonCodecConfiguration.scala | 63 ++++++++++++++++++- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 0e5a40a35..106fe18c8 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -243,8 +243,11 @@ object BuildHelper { incOptions ~= (_.withLogRecompileOnMacro(false)), autoAPIMappings := true, unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library"), - mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% name.value % _).toSet, - mimaCheckDirection := "backward", // TODO: find how we can use "both" for path versions + mimaPreviousArtifacts := { + previousStableVersion.value.map(organization.value %% name.value % _).toSet ++ + Set(organization.value %% name.value % "0.7.3") + }, + mimaCheckDirection := "backward", // TODO: find how we can use "both" for path versions mimaBinaryIssueFilters ++= Seq( exclude[Problem]("zio.json.macros#package."), exclude[Problem]("zio.JsonPackagePlatformSpecific.*"), diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index a6b34de64..a5e7989f3 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -233,7 +233,7 @@ private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A } } -final class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonDecoder] { self => +sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonDecoder] { self => def join[A](ctx: CaseClass[Typeclass, A]): JsonDecoder[A] = { val (transformNames, nameTransform): (Boolean, String => String) = ctx.annotations.collectFirst { case jsonMemberNames(format) => format } @@ -518,14 +518,20 @@ private lazy val caseObjectEncoder = new JsonEncoder[Any] { Right(Json.Obj(Chunk.empty)) } -object DeriveJsonDecoder { +object DeriveJsonDecoder extends JsonDecoderDerivation(JsonCodecConfiguration.default) { self => inline def gen[A](using config: JsonCodecConfiguration, mirror: Mirror.Of[A]) = { val derivation = new JsonDecoderDerivation(config) derivation.derived[A] } + + // Backcompat for 2.12, otherwise we'd use ArraySeq.unsafeWrapArray + private final class ArraySeq(p: Array[Any]) extends IndexedSeq[Any] { + def apply(i: Int): Any = p(i) + def length: Int = p.length + } } -final class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonEncoder] { self => +sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonEncoder] { self => def join[A](ctx: CaseClass[Typeclass, A]): JsonEncoder[A] = if (ctx.params.isEmpty) { caseObjectEncoder.narrow[A] @@ -772,7 +778,7 @@ final class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriva } } -object DeriveJsonEncoder { +object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.default) { self => inline def gen[A](using config: JsonCodecConfiguration, mirror: Mirror.Of[A]) = { val derivation = new JsonEncoderDerivation(config) derivation.derived[A] diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala index 2619f54bc..3c82e8125 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala @@ -22,9 +22,70 @@ final case class JsonCodecConfiguration( sumTypeMapping: JsonMemberFormat = IdentityFormat, explicitNulls: Boolean = false, explicitEmptyCollections: Boolean = true -) +) { + def this( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = this( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + true + ) + + def copy( + sumTypeHandling: SumTypeHandling = WrapperWithClassNameField.asInstanceOf[SumTypeHandling], + fieldNameMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], + allowExtraFields: Boolean = true, + sumTypeMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], + explicitNulls: Boolean = false, + explicitEmptyCollections: Boolean = true + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections + ) + + def copy( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + true + ) +} object JsonCodecConfiguration { + def apply( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + true + ) + implicit val default: JsonCodecConfiguration = JsonCodecConfiguration() sealed trait SumTypeHandling { From 5fbae3058744f9ece5f770e194d8577ae9ab36bd Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 18 Jan 2025 12:00:53 +0100 Subject: [PATCH 074/311] Code clean up by using `Lexer.error` (#1223) --- .../src/main/scala/zio/json/jsonDerive.scala | 11 ++- .../scala/zio/json/data/geojson/GeoJSON.scala | 85 +++++-------------- .../src/main/scala-2.x/zio/json/macros.scala | 74 +++++++--------- .../src/main/scala-3/zio/json/macros.scala | 68 ++++++--------- .../src/main/scala/zio/json/JsonDecoder.scala | 69 +++++++-------- .../scala/zio/json/JsonFieldDecoder.scala | 8 +- .../src/main/scala/zio/json/ast/ast.scala | 16 ++-- .../main/scala/zio/json/internal/lexer.scala | 6 +- 8 files changed, 126 insertions(+), 211 deletions(-) diff --git a/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala b/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala index ed7dc1f80..f92704319 100644 --- a/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala +++ b/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala @@ -128,10 +128,13 @@ private[json] final class DeriveCodecMacros(val c: blackbox.Context) { } else { val tparamNames = tparams.map(_.name) def mkImplicitParams(prefix: String, typeSymbol: TypeSymbol) = - tparamNames.zipWithIndex.map { case (tparamName, i) => - val paramName = TermName(s"$prefix$i") - val paramType = tq"$typeSymbol[$tparamName]" - q"$paramName: $paramType" + tparamNames.map { + var i = 0 + tparamName => + val paramName = TermName(s"$prefix$i") + i += 1 + val paramType = tq"$typeSymbol[$tparamName]" + q"$paramName: $paramType" } val decodeParams = mkImplicitParams("decode", DecoderClass) val encodeParams = mkImplicitParams("encode", EncoderClass) diff --git a/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala index cde606474..9cbca8d9b 100644 --- a/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala +++ b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala @@ -127,7 +127,7 @@ package handrolled { implicit lazy val zioJsonJsonDecoder: JsonDecoder[Geometry] = new JsonDecoder[Geometry] { import zio.json._ - import JsonDecoder.{ JsonError, UnsafeJson } + import JsonDecoder.JsonError import internal._ import scala.annotation._ @@ -157,10 +157,7 @@ package handrolled { case Json.Arr(chunk) if chunk.length == 2 && chunk(0).isInstanceOf[Json.Num] && chunk(1).isInstanceOf[Json.Num] => (chunk(0).asInstanceOf[Json.Num].value.doubleValue(), chunk(1).asInstanceOf[Json.Num].value.doubleValue()) - case _ => - throw UnsafeJson( - JsonError.Message("expected coordinates") :: trace - ) + case _ => Lexer.error("expected coordinates", trace) } def coordinates1( trace: List[JsonError], @@ -168,8 +165,7 @@ package handrolled { ): List[(Double, Double)] = js.elements.map { case js1: Json.Arr => coordinates0(trace, js1) - case _ => - throw UnsafeJson(JsonError.Message("expected list") :: trace) + case _ => Lexer.error("expected list", trace) }.toList def coordinates2( trace: List[JsonError], @@ -177,8 +173,7 @@ package handrolled { ): List[List[(Double, Double)]] = js.elements.map { case js1: Json.Arr => coordinates1(trace, js1) - case _ => - throw UnsafeJson(JsonError.Message("expected list") :: trace) + case _ => Lexer.error("expected list", trace) }.toList def coordinates3( trace: List[JsonError], @@ -186,8 +181,7 @@ package handrolled { ): List[List[List[(Double, Double)]]] = js.elements.map { case js1: Json.Arr => coordinates2(trace, js1) - case _ => - throw UnsafeJson(JsonError.Message("expected list") :: trace) + case _ => Lexer.error("expected list", trace) }.toList def unsafeDecode( @@ -208,40 +202,25 @@ package handrolled { val trace_ = spans(field) :: trace (field: @switch) match { case 0 => - if (subtype != -1) - throw UnsafeJson(JsonError.Message("duplicate") :: trace_) + if (subtype != -1) Lexer.error("duplicate", trace_) subtype = Lexer.enumeration(trace_, in, subtypes) case 1 => - if (coordinates != null) - throw UnsafeJson(JsonError.Message("duplicate") :: trace_) + if (coordinates != null) Lexer.error("duplicate", trace_) coordinates = coordinatesD.unsafeDecode(trace_, in) case 2 => - if (geometries != null) - throw UnsafeJson(JsonError.Message("duplicate") :: trace_) - + if (geometries != null) Lexer.error("duplicate", trace_) geometries = geometriesD.unsafeDecode(trace_, in) } } Lexer.nextField(trace, in) }) () - if (subtype == -1) - throw UnsafeJson( - JsonError.Message("missing discriminator") :: trace - ) - + if (subtype == -1) Lexer.error("missing discriminator", trace) if (subtype == 6) { - if (geometries == null) - throw UnsafeJson( - JsonError.Message("missing 'geometries' field") :: trace - ) + if (geometries == null) Lexer.error("missing 'geometries' field", trace) else GeometryCollection(geometries) } - - if (coordinates == null) - throw UnsafeJson( - JsonError.Message("missing 'coordinates' field") :: trace - ) + if (coordinates == null) Lexer.error("missing 'coordinates' field", trace) val trace_ = spans(1) :: trace (subtype: @switch) match { case 0 => Point(coordinates0(trace_, coordinates)) @@ -298,7 +277,7 @@ package handrolled { implicit lazy val zioJsonJsonDecoder: JsonDecoder[GeoJSON] = new JsonDecoder[GeoJSON] { import zio.json._ - import JsonDecoder.{ JsonError, UnsafeJson } + import JsonDecoder.JsonError import internal._ import scala.annotation._ @@ -332,52 +311,30 @@ package handrolled { val trace_ = spans(field) :: trace (field: @switch) match { case 0 => - if (subtype != -1) - throw UnsafeJson(JsonError.Message("duplicate") :: trace_) - + if (subtype != -1) Lexer.error("duplicate", trace_) subtype = Lexer.enumeration(trace_, in, subtypes) case 1 => - if (properties != null) - throw UnsafeJson(JsonError.Message("duplicate") :: trace_) - + if (properties != null) Lexer.error("duplicate", trace_) properties = propertyD.unsafeDecode(trace_, in) case 2 => - if (geometry != null) - throw UnsafeJson(JsonError.Message("duplicate") :: trace_) - + if (geometry != null) Lexer.error("duplicate", trace_) geometry = geometryD.unsafeDecode(trace_, in) case 3 => - if (features != null) - throw UnsafeJson(JsonError.Message("duplicate") :: trace_) - + if (features != null) Lexer.error("duplicate", trace_) features = featuresD.unsafeDecode(trace_, in) } } Lexer.nextField(trace, in) }) () - if (subtype == -1) - // we could infer the type but that would mean accepting invalid data - throw UnsafeJson( - JsonError.Message("missing required fields") :: trace - ) - + // we could infer the type but that would mean accepting invalid data + if (subtype == -1) Lexer.error("missing required fields", trace) if (subtype == 0) { - if (properties == null) - throw UnsafeJson( - JsonError.Message("missing 'properties' field") :: trace - ) - if (geometry == null) - throw UnsafeJson( - JsonError.Message("missing 'geometry' field") :: trace - ) + if (properties == null) Lexer.error("missing 'properties' field", trace) + if (geometry == null) Lexer.error("missing 'geometry' field", trace) Feature(properties, geometry) } else { - - if (features == null) - throw UnsafeJson( - JsonError.Message("missing 'features' field") :: trace - ) + if (features == null) Lexer.error("missing 'features' field", trace) FeatureCollection(features) } } diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 9d6351c6a..eb864e534 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -2,7 +2,7 @@ package zio.json import magnolia1._ import zio.Chunk -import zio.json.JsonDecoder.{ JsonError, UnsafeJson } +import zio.json.JsonDecoder.JsonError import zio.json.ast.Json import zio.json.internal.{ Lexer, RetractReader, StringMatrix, Write } @@ -228,21 +228,24 @@ object DeriveJsonDecoder { json match { case Json.Obj(_) => ctx.rawConstruct(Nil) case Json.Null => ctx.rawConstruct(Nil) - case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) + case _ => Lexer.error("Not an object", trace) } } else new JsonDecoder[A] { val (names, aliases): (Array[String], Array[(String, Int)]) = { - val names = Array.ofDim[String](ctx.parameters.size) + val names = new Array[String](ctx.parameters.size) val aliasesBuilder = Array.newBuilder[(String, Int)] - ctx.parameters.zipWithIndex.foreach { case (p, i) => - names(i) = p.annotations.collectFirst { case jsonField(name) => name } - .getOrElse(if (transformNames) nameTransform(p.label) else p.label) - aliasesBuilder ++= p.annotations.flatMap { - case jsonAliases(alias, aliases @ _*) => (alias +: aliases).map(_ -> i) - case _ => Seq.empty - } + ctx.parameters.foreach { + var i = 0 + p => + names(i) = p.annotations.collectFirst { case jsonField(name) => name } + .getOrElse(if (transformNames) nameTransform(p.label) else p.label) + aliasesBuilder ++= p.annotations.flatMap { + case jsonAliases(alias, aliases @ _*) => (alias +: aliases).map(_ -> i) + case _ => Seq.empty + } + i += 1 } val aliases = aliasesBuilder.result() @@ -270,9 +273,6 @@ object DeriveJsonDecoder { lazy val namesMap: Map[String, Int] = (names.zipWithIndex ++ aliases).toMap - private[this] def error(message: String, trace: List[JsonError]): Nothing = - throw UnsafeJson(JsonError.Message(message) :: trace) - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') @@ -288,7 +288,7 @@ object DeriveJsonDecoder { do { val field = Lexer.field(trace, in, matrix) if (field != -1) { - if (ps(field) != null) error("duplicate", trace) + if (ps(field) != null) Lexer.error("duplicate", trace) val default = defaults(field) ps(field) = if ( @@ -298,8 +298,8 @@ object DeriveJsonDecoder { } ) tcs(field).unsafeDecode(spans(field) :: trace, in) else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get - else error("expected 'null'", spans(field) :: trace) - } else if (no_extra) error("invalid extra field", trace) + else Lexer.error("expected 'null'", spans(field) :: trace) + } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) } while (Lexer.nextField(trace, in)) var i = 0 @@ -322,13 +322,13 @@ object DeriveJsonDecoder { for ((key, value) <- fields) { namesMap.get(key) match { case Some(field) => - if (ps(field) != null) error("duplicate", trace) + if (ps(field) != null) Lexer.error("duplicate", trace) ps(field) = { if ((value eq Json.Null) && (defaults(field) ne None)) defaults(field).get else tcs(field).unsafeFromJsonAST(spans(field) :: trace, value) } case _ => - if (no_extra) error("invalid extra field", trace) + if (no_extra) Lexer.error("invalid extra field", trace) } } var i = 0 @@ -341,7 +341,7 @@ object DeriveJsonDecoder { i += 1 } ctx.rawConstruct(new ArraySeq(ps)) - case _ => error("Not an object", trace) + case _ => Lexer.error("Not an object", trace) } } } @@ -374,14 +374,8 @@ object DeriveJsonDecoder { val a = tcs(field).unsafeDecode(trace_, in).asInstanceOf[A] Lexer.char(trace, in, '}') a - } else - throw UnsafeJson( - JsonError.Message("invalid disambiguator") :: trace - ) - } else - throw UnsafeJson( - JsonError.Message("expected non-empty object") :: trace - ) + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.error("expected non-empty object", trace) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = @@ -391,10 +385,10 @@ object DeriveJsonDecoder { namesMap.get(key) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(JsonError.ObjectAccess(key) :: trace, inner).asInstanceOf[A] - case None => throw UnsafeJson(JsonError.Message("Invalid disambiguator") :: trace) + case None => Lexer.error("Invalid disambiguator", trace) } - case Json.Obj(_) => throw UnsafeJson(JsonError.Message("Not an object with a single field") :: trace) - case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) + case Json.Obj(_) => Lexer.error("Not an object with a single field", trace) + case _ => Lexer.error("Not an object", trace) } } else @@ -410,20 +404,14 @@ object DeriveJsonDecoder { do { if (Lexer.field(trace, in_, hintmatrix) != -1) { val field = Lexer.enumeration(trace, in_, matrix) - if (field == -1) - throw UnsafeJson( - JsonError.Message(s"invalid disambiguator") :: trace - ) + if (field == -1) Lexer.error("invalid disambiguator", trace) in_.rewind() val trace_ = spans(field) :: trace return tcs(field).unsafeDecode(trace_, in_).asInstanceOf[A] } else Lexer.skipValue(trace, in_) } while (Lexer.nextField(trace, in_)) - - throw UnsafeJson( - JsonError.Message(s"missing hint '$hintfield'") :: trace - ) + Lexer.error(s"missing hint '$hintfield'", trace) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = @@ -433,14 +421,12 @@ object DeriveJsonDecoder { case Some((_, Json.Str(name))) => namesMap.get(name) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(trace, json).asInstanceOf[A] - case None => throw UnsafeJson(JsonError.Message("Invalid disambiguator") :: trace) + case _ => Lexer.error("Invalid disambiguator", trace) } - case Some(_) => - throw UnsafeJson(JsonError.Message(s"Non-string hint '$hintfield'") :: trace) - case None => - throw UnsafeJson(JsonError.Message(s"Missing hint '$hintfield'") :: trace) + case Some(_) => Lexer.error(s"Non-string hint '$hintfield'", trace) + case _ => Lexer.error(s"Missing hint '$hintfield'", trace) } - case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) + case _ => Lexer.error("Not an object", trace) } } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index a5e7989f3..1182ec793 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -8,7 +8,7 @@ import scala.compiletime.* import scala.reflect.* import zio.Chunk -import zio.json.JsonDecoder.{ JsonError, UnsafeJson } +import zio.json.JsonDecoder.JsonError import zio.json.ast.Json import zio.json.internal.{ Lexer, RetractReader, StringMatrix, Write } @@ -229,7 +229,7 @@ private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A json match { case Json.Obj(_) => ctx.rawConstruct(Nil) case Json.Null => ctx.rawConstruct(Nil) - case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) + case _ => Lexer.error("Not an object", trace) } } @@ -295,9 +295,6 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv lazy val namesMap: Map[String, Int] = (names.zipWithIndex ++ aliases).toMap - private[this] def error(message: String, trace: List[JsonError]): Nothing = - throw UnsafeJson(JsonError.Message(message) :: trace) - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') val ps = new Array[Any](len) @@ -305,15 +302,15 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv while({ val field = Lexer.field(trace, in, matrix) if (field != -1) { - if (ps(field) != null) error("duplicate", trace) + if (ps(field) != null) Lexer.error("duplicate", trace) val default = defaults(field) ps(field) = if ((default eq None) || in.nextNonWhitespace() != 'n' && { in.retract() true }) tcs(field).unsafeDecode(spans(field) :: trace, in) else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get - else error("expected 'null'", spans(field) :: trace) - } else if (no_extra) error("invalid extra field", trace) + else Lexer.error("expected 'null'", spans(field) :: trace) + } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) Lexer.nextField(trace, in) }) () @@ -336,13 +333,13 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv for ((key, value) <- fields) { namesMap.get(key) match { case Some(field) => - if (ps(field) != null) error("duplicate", trace) + if (ps(field) != null) Lexer.error("duplicate", trace) ps(field) = { if ((value eq Json.Null) && (defaults(field) ne None)) defaults(field).get else tcs(field).unsafeFromJsonAST(spans(field) :: trace, value) } case _ => - if (no_extra) error("invalid extra field", trace) + if (no_extra) Lexer.error("invalid extra field", trace) } } var i = 0 @@ -355,7 +352,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv i += 1 } ctx.rawConstruct(new ArraySeq(ps)) - case _ => error("Not an object", trace) + case _ => Lexer.error("Not an object", trace) } } } @@ -391,7 +388,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv val typeName = Lexer.string(trace, in).toString() namesMap.find(_._1 == typeName) match { case Some((_, idx)) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - case None => throw UnsafeJson(JsonError.Message(s"Invalid enumeration value $typeName") :: trace) + case None => Lexer.error(s"Invalid enumeration value $typeName", trace) } } @@ -400,9 +397,9 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv case Json.Str(typeName) => ctx.subtypes.find(_.typeInfo.short == typeName) match { case Some(sub) => sub.typeclass.asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - case None => throw UnsafeJson(JsonError.Message(s"Invalid enumeration value $typeName") :: trace) + case None => Lexer.error(s"Invalid enumeration value $typeName", trace) } - case _ => throw UnsafeJson(JsonError.Message("Not a string") :: trace) + case _ => Lexer.error("Not a string", trace) } } } @@ -422,14 +419,8 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv val a = tcs(field).unsafeDecode(trace_, in).asInstanceOf[A] Lexer.char(trace, in, '}') a - } else - throw UnsafeJson( - JsonError.Message("invalid disambiguator") :: trace - ) - } else - throw UnsafeJson( - JsonError.Message("expected non-empty object") :: trace - ) + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.error("expected non-empty object", trace) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = { @@ -438,10 +429,10 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv val (key, inner) = chunk.head namesMap.get(key) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(JsonError.ObjectAccess(key) :: trace, inner).asInstanceOf[A] - case None => throw UnsafeJson(JsonError.Message("Invalid disambiguator") :: trace) + case None => Lexer.error("Invalid disambiguator", trace) } - case Json.Obj(_) => throw UnsafeJson(JsonError.Message("Not an object with a single field") :: trace) - case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) + case Json.Obj(_) => Lexer.error("Not an object with a single field", trace) + case _ => Lexer.error("Not an object", trace) } } } @@ -453,31 +444,20 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val in_ = zio.json.internal.RecordingReader(in) - Lexer.char(trace, in_, '{') - if (Lexer.firstField(trace, in_)) { - while({ + while ({ if (Lexer.field(trace, in_, hintmatrix) != -1) { val field = Lexer.enumeration(trace, in_, matrix) - - if (field == -1) { - throw UnsafeJson(JsonError.Message(s"invalid disambiguator") :: trace) - } - + if (field == -1) Lexer.error("invalid disambiguator", trace) in_.rewind() val trace_ = spans(field) :: trace - return tcs(field).unsafeDecode(trace_, in_).asInstanceOf[A] - } else { - Lexer.skipValue(trace, in_) - } - + } else Lexer.skipValue(trace, in_) Lexer.nextField(trace, in_) }) () } - - throw UnsafeJson(JsonError.Message(s"missing hint '$hintfield'") :: trace) + Lexer.error(s"missing hint '$hintfield'", trace) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = { @@ -487,14 +467,14 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv case Some((_, Json.Str(name))) => namesMap.get(name) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(JsonError.ObjectAccess(name) :: trace, json).asInstanceOf[A] - case None => throw UnsafeJson(JsonError.Message("Invalid disambiguator") :: trace) + case None => Lexer.error("Invalid disambiguator", trace) } case Some(_) => - throw UnsafeJson(JsonError.Message(s"Non-string hint '$hintfield'") :: trace) + Lexer.error(s"Non-string hint '$hintfield'", trace) case None => - throw UnsafeJson(JsonError.Message(s"Missing hint '$hintfield'") :: trace) + Lexer.error(s"Missing hint '$hintfield'", trace) } - case _ => throw UnsafeJson(JsonError.Message("Not an object") :: trace) + case _ => Lexer.error("Not an object", trace) } } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 1ec56706d..c7a0f09e5 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -157,22 +157,20 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): B = f(self.unsafeDecode(trace, in)) match { - case Left(err) => - throw JsonDecoder.UnsafeJson(JsonError.Message(err) :: trace) - case Right(b) => b + case Left(err) => Lexer.error(err, trace) + case Right(b) => b } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): B = f(self.unsafeFromJsonAST(trace, json)) match { - case Left(err) => throw JsonDecoder.UnsafeJson(JsonError.Message(err) :: trace) + case Left(err) => Lexer.error(err, trace) case Right(b) => b } override def unsafeDecodeMissing(trace: List[JsonError]): B = f(self.unsafeDecodeMissing(trace)) match { - case Left(err) => - throw JsonDecoder.UnsafeJson(JsonError.Message(err) :: trace) - case Right(b) => b + case Left(err) => Lexer.error(err, trace) + case Right(b) => b } } @@ -200,7 +198,7 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { self.zip(that).map(f.tupled) def unsafeDecodeMissing(trace: List[JsonError]): A = - throw JsonDecoder.UnsafeJson(JsonError.Message("missing") :: trace) + Lexer.error("missing", trace) /** * Low-level, unsafe method to decode a value or throw an exception. This method should not be called in application @@ -249,9 +247,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with if (partialFunction.isDefinedAt(c)) { in.retract() partialFunction(c).unsafeDecode(trace, in) - } else { - throw UnsafeJson(JsonError.Message(s"missing case in `peekChar` for '${c}''") :: trace) - } + } else Lexer.error(s"missing case in `peekChar` for '${c}''", trace) } } @@ -275,7 +271,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): String = json match { case Json.Str(value) => value - case _ => throw UnsafeJson(JsonError.Message("Not a string value") :: trace) + case _ => Lexer.error("Not a string value", trace) } } @@ -287,7 +283,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Boolean = json match { case Json.Bool(value) => value - case _ => throw UnsafeJson(JsonError.Message("Not a bool value") :: trace) + case _ => Lexer.error("Not a bool value", trace) } } @@ -331,13 +327,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case Json.Num(value) => try fromBigDecimal(value) catch { - case exception: ArithmeticException => throw UnsafeJson(JsonError.Message(exception.getMessage) :: trace) + case exception: ArithmeticException => Lexer.error(exception.getMessage, trace) } case Json.Str(value) => val reader = new FastStringReader(value) try f(List.empty, reader) finally reader.close() - case _ => throw UnsafeJson(JsonError.Message("Not a number or a string") :: trace) + case _ => Lexer.error("Not a number or a string", trace) } } @@ -351,7 +347,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with def unsafeDecode(trace: List[JsonError], in: RetractReader): Option[A] = if (in.nextNonWhitespace() == 'n') { - if (in.readChar() != 'u' || in.readChar() != 'l' || in.readChar() != 'l') error(trace) + if (in.readChar() != 'u' || in.readChar() != 'l' || in.readChar() != 'l') { + Lexer.error("expected 'null'", trace) + } None } else { in.retract() @@ -361,9 +359,6 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Option[A] = if (json eq Json.Null) None else new Some(A.unsafeFromJsonAST(trace, json)) - - private[this] def error(trace: List[JsonError]): Option[A] = - throw UnsafeJson(JsonError.Message("expected 'null'") :: trace) } // supports multiple representations for compatibility with other libraries, @@ -385,9 +380,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with in: RetractReader ): Either[A, B] = { Lexer.char(trace, in, '{') - - val values: Array[Any] = Array.ofDim(2) - + val values = new Array[Any](2) if (Lexer.firstField(trace, in)) while ({ { @@ -396,26 +389,18 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with else { val trace_ = spans(field) :: trace if (field < 3) { - if (values(0) != null) - throw UnsafeJson(JsonError.Message("duplicate") :: trace_) + if (values(0) != null) Lexer.error("duplicate", trace_) values(0) = A.unsafeDecode(trace_, in) } else { - if (values(1) != null) - throw UnsafeJson(JsonError.Message("duplicate") :: trace_) + if (values(1) != null) Lexer.error("duplicate", trace_) values(1) = B.unsafeDecode(trace_, in) } } }; Lexer.nextField(trace, in) }) () - - if (values(0) == null && values(1) == null) - throw UnsafeJson(JsonError.Message("missing fields") :: trace) - if (values(0) != null && values(1) != null) - throw UnsafeJson( - JsonError.Message("ambiguous either, zip present") :: trace - ) - if (values(0) != null) - Left(values(0).asInstanceOf[A]) + if (values(0) == null && values(1) == null) Lexer.error("missing fields", trace) + if (values(0) != null && values(1) != null) Lexer.error("ambiguous either, zip present", trace) + if (values(0) != null) Left(values(0).asInstanceOf[A]) else Right(values(1).asInstanceOf[B]) } } @@ -461,13 +446,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = f(string.unsafeDecode(trace, in)) match { - case Left(err) => throw UnsafeJson(JsonError.Message(err) :: trace) + case Left(err) => Lexer.error(err, trace) case Right(value) => value } override def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = f(string.unsafeFromJsonAST(trace, json)) match { - case Left(err) => throw UnsafeJson(JsonError.Message(err) :: trace) + case Left(err) => Lexer.error(err, trace) case Right(value) => value } } @@ -507,10 +492,14 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = json match { case Json.Arr(elements) => - elements.zipWithIndex.map { case (json, i) => - decoder.unsafeFromJsonAST(JsonError.ArrayAccess(i) :: trace, json) + elements.map { + var i = 0 + json => + val span = JsonError.ArrayAccess(i) + i += 1 + decoder.unsafeFromJsonAST(span :: trace, json) } - case _ => throw UnsafeJson(JsonError.Message("Not an array") :: trace) + case _ => Lexer.error("Not an array", trace) } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala index d25f6adf7..88148978a 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala @@ -15,6 +15,7 @@ */ package zio.json +import zio.json.internal.Lexer import zio.json.uuid.UUIDParser /** When decoding a JSON Object, we only allow the keys that implement this interface. */ @@ -33,9 +34,8 @@ trait JsonFieldDecoder[+A] { def unsafeDecodeField(trace: List[JsonError], in: String): B = f(self.unsafeDecodeField(trace, in)) match { - case Left(err) => - throw JsonDecoder.UnsafeJson(JsonError.Message(err) :: trace) - case Right(b) => b + case Left(err) => Lexer.error(err, trace) + case Right(b) => b } } @@ -80,7 +80,7 @@ object JsonFieldDecoder { new JsonFieldDecoder[A] { def unsafeDecodeField(trace: List[JsonError], in: String): A = f(string.unsafeDecodeField(trace, in)) match { - case Left(err) => throw JsonDecoder.UnsafeJson(JsonError.Message(err) :: trace) + case Left(err) => Lexer.error(err, trace) case Right(value) => value } } diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index a1f44f21a..3fac2459e 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -16,7 +16,7 @@ package zio.json.ast import zio.Chunk -import zio.json.JsonDecoder.{ JsonError, UnsafeJson } +import zio.json.JsonDecoder.JsonError import zio.json._ import zio.json.ast.Json._ import zio.json.internal._ @@ -388,7 +388,7 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Obj = json match { case obj @ Obj(_) => obj - case _ => throw UnsafeJson(JsonError.Message(s"Not an object") :: trace) + case _ => Lexer.error("Not an object", trace) } } private lazy val obje = JsonEncoder.keyValueChunk[String, Json] @@ -433,7 +433,7 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Arr = json match { case arr @ Arr(_) => arr - case _ => throw UnsafeJson(JsonError.Message(s"Not an array") :: trace) + case _ => Lexer.error("Not an array", trace) } } private lazy val arre = JsonEncoder.chunk[Json] @@ -462,7 +462,7 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Bool = json match { case b @ Bool(_) => b - case _ => throw UnsafeJson(JsonError.Message(s"Not a bool value") :: trace) + case _ => Lexer.error("Not a bool value", trace) } } implicit val encoder: JsonEncoder[Bool] = new JsonEncoder[Bool] { @@ -486,7 +486,7 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Str = json match { case s @ Str(_) => s - case _ => throw UnsafeJson(JsonError.Message(s"Not a string value") :: trace) + case _ => Lexer.error("Not a string value", trace) } } implicit val encoder: JsonEncoder[Str] = new JsonEncoder[Str] { @@ -518,7 +518,7 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Num = json match { case n @ Num(_) => n - case _ => throw UnsafeJson(JsonError.Message(s"Not a number") :: trace) + case _ => Lexer.error("Not a number", trace) } } implicit val encoder: JsonEncoder[Num] = new JsonEncoder[Num] { @@ -542,7 +542,7 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Null.type = json match { case Null => Null - case _ => throw UnsafeJson(JsonError.Message(s"Not null") :: trace) + case _ => Lexer.error("Not null", trace) } } implicit val encoder: JsonEncoder[Null.type] = new JsonEncoder[Null.type] { @@ -570,7 +570,7 @@ object Json { case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => Num.decoder.unsafeDecode(trace, in) case c => - throw UnsafeJson(JsonError.Message(s"unexpected '$c'") :: trace) + Lexer.error(s"unexpected '$c'", trace) } } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 984363e78..12e03d9f7 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -34,7 +34,7 @@ object Lexer { @noinline private[json] def error(expected: String, got: Char, trace: List[JsonError]): Nothing = - throw UnsafeJson(JsonError.Message(s"expected $expected got '$got'") :: trace) + error(s"expected $expected got '$got'", trace) @noinline private[json] def error(c: Char, trace: List[JsonError]): Nothing = @@ -274,10 +274,10 @@ object Lexer { val c4 = in.readChar() (c1: @switch) match { case 't' => - if (c2 != 'r' || c3 != 'u' || c4 != 'e') error(s"expected 'true'", trace) + if (c2 != 'r' || c3 != 'u' || c4 != 'e') error("expected 'true'", trace) true case 'f' => - if (in.readChar() != 'e' || c2 != 'a' || c3 != 'l' || c4 != 's') error(s"expected 'false'", trace) + if (in.readChar() != 'e' || c2 != 'a' || c3 != 'l' || c4 != 's') error("expected 'false'", trace) false case c => error("'true' or 'false'", c, trace) From 75ec28b7b4646d0108a26cb03e5a26251f20988b Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 19 Jan 2025 09:54:40 +0100 Subject: [PATCH 075/311] More efficient encoding of product types (#1224) * More efficient decoding of sum types * Add missing assertions for usage of JSON hint annotations in enum definitions with Scala 3 * More efficient decoding and encoding sum and product types --- .../scala/zio/json/data/twitter/Twitter.scala | 1 - .../src/main/scala-2.x/zio/json/macros.scala | 293 +++++++------ .../src/main/scala-3/zio/json/macros.scala | 402 ++++++++---------- .../src/main/scala/zio/json/JsonDecoder.scala | 27 +- .../scala-3/zio/json/DerivedDecoderSpec.scala | 28 +- 5 files changed, 362 insertions(+), 389 deletions(-) diff --git a/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala b/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala index 3ec2e647c..de2f75a38 100644 --- a/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala +++ b/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala @@ -1,7 +1,6 @@ package zio.json.data.twitter import com.github.ghik.silencer.silent -import io.circe import io.circe.Codec import io.circe.generic.semiauto.deriveCodec import zio.json._ diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index eb864e534..bc451f495 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -233,22 +233,21 @@ object DeriveJsonDecoder { } else new JsonDecoder[A] { - val (names, aliases): (Array[String], Array[(String, Int)]) = { + private[this] val (names, aliases): (Array[String], Array[(String, Int)]) = { val names = new Array[String](ctx.parameters.size) val aliasesBuilder = Array.newBuilder[(String, Int)] ctx.parameters.foreach { - var i = 0 + var idx = 0 p => - names(i) = p.annotations.collectFirst { case jsonField(name) => name } + names(idx) = p.annotations.collectFirst { case jsonField(name) => name } .getOrElse(if (transformNames) nameTransform(p.label) else p.label) aliasesBuilder ++= p.annotations.flatMap { - case jsonAliases(alias, aliases @ _*) => (alias +: aliases).map(_ -> i) + case jsonAliases(alias, aliases @ _*) => (alias +: aliases).map(_ -> idx) case _ => Seq.empty } - i += 1 + idx += 1 } - val aliases = aliasesBuilder.result() - + val aliases = aliasesBuilder.result() val allFieldNames = names ++ aliases.map(_._1) if (allFieldNames.length != allFieldNames.distinct.length) { val aliasNames = aliases.map(_._1) @@ -259,19 +258,15 @@ object DeriveJsonDecoder { s"alias(es) ${collisions.mkString(",")} collide with a field or another alias" throw new AssertionError(msg) } - (names, aliases) } - - val len: Int = names.length - val matrix: StringMatrix = new StringMatrix(names, aliases) - val spans: Array[JsonError] = names.map(JsonError.ObjectAccess) - lazy val tcs: Array[JsonDecoder[Any]] = + private[this] val len = names.length + private[this] val matrix = new StringMatrix(names, aliases) + private[this] val spans = names.map(JsonError.ObjectAccess) + private[this] val defaults = ctx.parameters.map(_.default).toArray + private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] - lazy val defaults: Array[Option[Any]] = - ctx.parameters.map(_.default).toArray - lazy val namesMap: Map[String, Int] = - (names.zipWithIndex ++ aliases).toMap + private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') @@ -286,30 +281,31 @@ object DeriveJsonDecoder { val ps = new Array[Any](len) if (Lexer.firstField(trace, in)) do { - val field = Lexer.field(trace, in, matrix) - if (field != -1) { - if (ps(field) != null) Lexer.error("duplicate", trace) - val default = defaults(field) - ps(field) = + val idx = Lexer.field(trace, in, matrix) + if (idx != -1) { + if (ps(idx) != null) Lexer.error("duplicate", trace) + val default = defaults(idx) + ps(idx) = if ( (default eq None) || in.nextNonWhitespace() != 'n' && { in.retract() true } - ) tcs(field).unsafeDecode(spans(field) :: trace, in) + ) tcs(idx).unsafeDecode(spans(idx) :: trace, in) else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get - else Lexer.error("expected 'null'", spans(field) :: trace) + else Lexer.error("expected 'null'", spans(idx) :: trace) } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) } while (Lexer.nextField(trace, in)) - var i = 0 - while (i < len) { - if (ps(i) == null) { - ps(i) = - if (defaults(i) ne None) defaults(i).get - else tcs(i).unsafeDecodeMissing(spans(i) :: trace) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne None) default.get + else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } - i += 1 + idx += 1 } ctx.rawConstruct(new ArraySeq(ps)) @@ -317,28 +313,29 @@ object DeriveJsonDecoder { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(fields) => + case Json.Obj(keyValues) => val ps = new Array[Any](len) - for ((key, value) <- fields) { + for ((key, value) <- keyValues) { namesMap.get(key) match { - case Some(field) => - if (ps(field) != null) Lexer.error("duplicate", trace) - ps(field) = { - if ((value eq Json.Null) && (defaults(field) ne None)) defaults(field).get - else tcs(field).unsafeFromJsonAST(spans(field) :: trace, value) - } + case Some(idx) => + if (ps(idx) != null) Lexer.error("duplicate", trace) + val default = defaults(idx) + ps(idx) = + if ((default ne None) && (value eq Json.Null)) default.get + else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, value) case _ => if (no_extra) Lexer.error("invalid extra field", trace) } } - var i = 0 - while (i < len) { - if (ps(i) == null) { - ps(i) = - if (defaults(i) ne None) defaults(i).get - else tcs(i).unsafeDecodeMissing(spans(i) :: trace) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne None) default.get + else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } - i += 1 + idx += 1 } ctx.rawConstruct(new ArraySeq(ps)) case _ => Lexer.error("Not an object", trace) @@ -347,31 +344,29 @@ object DeriveJsonDecoder { } def split[A](ctx: SealedTrait[JsonDecoder, A])(implicit config: JsonCodecConfiguration): JsonDecoder[A] = { - val jsonHintFormat: JsonMemberFormat = + val jsonHintFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) - val names: Array[String] = ctx.subtypes.map { p => - p.annotations.collectFirst { case jsonHint(name) => - name - }.getOrElse(jsonHintFormat(p.typeName.short)) + val names = ctx.subtypes.map { p => + p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeName.short)) }.toArray - val matrix: StringMatrix = new StringMatrix(names) - lazy val tcs: Array[JsonDecoder[Any]] = - ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] - lazy val namesMap: Map[String, Int] = names.zipWithIndex.toMap + val matrix = new StringMatrix(names) + lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] + lazy val namesMap = names.zipWithIndex.toMap def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) + if (discrim.isEmpty) new JsonDecoder[A] { - val spans: Array[JsonError] = names.map(JsonError.ObjectAccess) + private[this] val spans = names.map(JsonError.ObjectAccess) + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') // we're not allowing extra fields in this encoding if (Lexer.firstField(trace, in)) { - val field = Lexer.field(trace, in, matrix) - if (field != -1) { - val trace_ = spans(field) :: trace - val a = tcs(field).unsafeDecode(trace_, in).asInstanceOf[A] + val idx = Lexer.field(trace, in, matrix) + if (idx != -1) { + val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] Lexer.char(trace, in, '}') a } else Lexer.error("invalid disambiguator", trace) @@ -381,11 +376,10 @@ object DeriveJsonDecoder { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case Json.Obj(chunk) if chunk.size == 1 => - val (key, inner) = chunk.head - namesMap.get(key) match { - case Some(idx) => - tcs(idx).unsafeFromJsonAST(JsonError.ObjectAccess(key) :: trace, inner).asInstanceOf[A] - case None => Lexer.error("Invalid disambiguator", trace) + val keyValue = chunk.head + namesMap.get(keyValue._1) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, keyValue._2).asInstanceOf[A] + case _ => Lexer.error("Invalid disambiguator", trace) } case Json.Obj(_) => Lexer.error("Not an object with a single field", trace) case _ => Lexer.error("Not an object", trace) @@ -393,9 +387,9 @@ object DeriveJsonDecoder { } else new JsonDecoder[A] { - val hintfield = discrim.get - val hintmatrix = new StringMatrix(Array(hintfield)) - val spans: Array[JsonError] = names.map(JsonError.Message) + private[this] val hintfield = discrim.get + private[this] val hintmatrix = new StringMatrix(Array(hintfield)) + private[this] val spans = names.map(JsonError.Message) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val in_ = internal.RecordingReader(in) @@ -403,13 +397,12 @@ object DeriveJsonDecoder { if (Lexer.firstField(trace, in_)) do { if (Lexer.field(trace, in_, hintmatrix) != -1) { - val field = Lexer.enumeration(trace, in_, matrix) - if (field == -1) Lexer.error("invalid disambiguator", trace) - in_.rewind() - val trace_ = spans(field) :: trace - return tcs(field).unsafeDecode(trace_, in_).asInstanceOf[A] - } else - Lexer.skipValue(trace, in_) + val idx = Lexer.enumeration(trace, in_, matrix) + if (idx != -1) { + in_.rewind() + return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.skipValue(trace, in_) } while (Lexer.nextField(trace, in_)) Lexer.error(s"missing hint '$hintfield'", trace) } @@ -417,7 +410,7 @@ object DeriveJsonDecoder { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case Json.Obj(fields) => - fields.find { case (k, _) => k == hintfield } match { + fields.find { case (key, _) => key == hintfield } match { case Some((_, Json.Str(name))) => namesMap.get(name) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(trace, json).asInstanceOf[A] @@ -447,73 +440,75 @@ object DeriveJsonEncoder { } else new JsonEncoder[A] { - val (transformNames, nameTransform): (Boolean, String => String) = + private[this] val (transformNames, nameTransform): (Boolean, String => String) = ctx.annotations.collectFirst { case jsonMemberNames(format) => format } .orElse(Some(config.fieldNameMapping)) .filter(_ != IdentityFormat) .map(true -> _) .getOrElse(false -> identity) - - val params = ctx.parameters + private[this] val params = ctx.parameters .filter(p => p.annotations.collectFirst { case _: jsonExclude => () }.isEmpty) .toArray - - val names: Array[String] = params.map { p => - p.annotations.collectFirst { case jsonField(name) => - name - }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) - } - - val explicitNulls: Boolean = + private[this] val names = + params.map { p => + p.annotations.collectFirst { case jsonField(name) => + name + }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) + } + private[this] val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - - val explicitEmptyCollections: Boolean = + private[this] val explicitEmptyCollections = ctx.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => enabled }.getOrElse(config.explicitEmptyCollections) - - lazy val tcs: Array[JsonEncoder[Any]] = params.map(p => p.typeclass.asInstanceOf[JsonEncoder[Any]]) - val len: Int = params.length + private[this] lazy val fields = params.map { + var idx = 0 + p => + val field = ( + p, + names(idx), + p.typeclass.asInstanceOf[JsonEncoder[Any]], + explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]), + p.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => + enabled + }.getOrElse(explicitEmptyCollections) + ) + idx += 1 + field + } override def isEmpty(a: A): Boolean = params.forall(p => p.typeclass.isEmpty(p.dereference(a))) def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { - var i = 0 - out.write("{") + out.write('{') val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) - + val fields = this.fields + var idx = 0 var prevFields = false // whether any fields have been written - while (i < len) { - val tc = tcs(i) - val p = params(i).dereference(a) - val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) - val writeEmptyCollections = - params(i).annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => - enabled - }.getOrElse(explicitEmptyCollections) - if ( - (!tc.isNothing(p) && !tc.isEmpty(p)) || (tc - .isNothing(p) && writeNulls) || (tc.isEmpty(p) && writeEmptyCollections) - ) { + while (idx < fields.length) { + val field = fields(idx) + val p = field._1.dereference(a) + if ({ + val isNothing = field._3.isNothing(p) + val isEmpty = field._3.isEmpty(p) + (!isNothing && !isEmpty) || (isNothing && field._4) || (isEmpty && field._5) + }) { // if we have at least one field already, we need a comma if (prevFields) { - if (indent.isEmpty) out.write(",") - else { - out.write(",") - JsonEncoder.pad(indent_, out) - } + out.write(',') + JsonEncoder.pad(indent_, out) } - JsonEncoder.string.unsafeEncode(names(i), indent_, out) - if (indent.isEmpty) out.write(":") + JsonEncoder.string.unsafeEncode(field._2, indent_, out) + if (indent.isEmpty) out.write(':') else out.write(" : ") - tc.unsafeEncode(p, indent_, out) + field._3.unsafeEncode(p, indent_, out) prevFields = true // record that we have at least one field so far } - i += 1 + idx += 1 } JsonEncoder.pad(indent, out) - out.write("}") + out.write('}') } override final def toJsonAST(a: A): Either[String, Json] = @@ -544,24 +539,24 @@ object DeriveJsonEncoder { val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val names: Array[String] = ctx.subtypes.map { p => - p.annotations.collectFirst { case jsonHint(name) => - name - }.getOrElse(jsonHintFormat(p.typeName.short)) + p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeName.short)) }.toArray + def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - if (discrim.isEmpty) + + if (discrim.isEmpty) { new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.split(a) { sub => - out.write("{") + out.write('{') val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) JsonEncoder.string.unsafeEncode(names(sub.index), indent_, out) - if (indent.isEmpty) out.write(":") + if (indent.isEmpty) out.write(':') else out.write(" : ") sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) JsonEncoder.pad(indent, out) - out.write("}") + out.write('}') } override def toJsonAST(a: A): Either[String, Json] = @@ -575,18 +570,18 @@ object DeriveJsonEncoder { } } } - else + } else { new JsonEncoder[A] { - val hintfield = discrim.get + private[this] val hintfield = discrim.get + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.split(a) { sub => - out.write("{") + out.write('{') val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) JsonEncoder.string.unsafeEncode(hintfield, indent_, out) - if (indent.isEmpty) out.write(":") + if (indent.isEmpty) out.write(':') else out.write(" : ") JsonEncoder.string.unsafeEncode(names(sub.index), indent_, out) - // whitespace is always off by 2 spaces at the end, probably not worth fixing val intermediate = new NestedWriter(out, indent_) sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) @@ -600,7 +595,7 @@ object DeriveJsonEncoder { } } } - + } } def gen[A]: JsonEncoder[A] = macro Magnolia.gen[A] @@ -615,24 +610,44 @@ private final class ArraySeq(p: Array[Any]) extends IndexedSeq[Any] { // intercepts the first `{` of a nested writer and discards it. We also need to // inject a `,` unless an empty object `{}` has been written. private[this] final class NestedWriter(out: Write, indent: Option[Int]) extends Write { - private[this] var first, second = true - - def write(c: Char): Unit = write(c.toString) // could be optimised + private[this] var state = 2 + + def write(c: Char): Unit = + if (state != 0) { + if (c == ' ' || c == '\n') { + () + } else if (state == 2 && c == '{') { + state = 1 + } else if (state == 1) { + state = 0 + if (c != '}') { + out.write(',') + JsonEncoder.pad(indent, out) + } + out.write(c) + } + } else out.write(c) def write(s: String): Unit = - if (first || second) { + if (state != 0) { var i = 0 while (i < s.length) { val c = s.charAt(i) - if (c == ' ' || c == '\n') {} else if (first && c == '{') { - first = false - } else if (second) { - second = false + if (c == ' ' || c == '\n') { + () + } else if (state == 2 && c == '{') { + state = 1 + } else if (state == 1) { + state = 0 if (c != '}') { out.write(',') JsonEncoder.pad(indent, out) } - return out.write(s.substring(i)) + while (i < s.length) { + out.write(s.charAt(i)) + i += 1 + } + return } i += 1 } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 1182ec793..4ab9af2c8 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -215,23 +215,21 @@ final class jsonNoExtraFields extends Annotation final class jsonExclude extends Annotation private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) extends JsonDecoder[A] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - if (no_extra) { - Lexer.char(trace, in, '{') - Lexer.char(trace, in, '}') - } else { - Lexer.skipValue(trace, in) - } - ctx.rawConstruct(Nil) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + if (no_extra) { + Lexer.char(trace, in, '{') + Lexer.char(trace, in, '}') + } else Lexer.skipValue(trace, in) + ctx.rawConstruct(Nil) + } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case Json.Obj(_) => ctx.rawConstruct(Nil) - case Json.Null => ctx.rawConstruct(Nil) - case _ => Lexer.error("Not an object", trace) - } - } + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case Json.Obj(_) => ctx.rawConstruct(Nil) + case Json.Null => ctx.rawConstruct(Nil) + case _ => Lexer.error("Not an object", trace) + } +} sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonDecoder] { self => def join[A](ctx: CaseClass[Typeclass, A]): JsonDecoder[A] = { @@ -251,23 +249,25 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv new CaseObjectDecoder(ctx, no_extra) } else { new JsonDecoder[A] { - val (names, aliases): (Array[String], Array[(String, Int)]) = { + private val (names, aliases): (Array[String], Array[(String, Int)]) = { val names = Array.ofDim[String](ctx.params.size) val aliasesBuilder = Array.newBuilder[(String, Int)] - ctx.params.zipWithIndex.foreach { (p, i) => - names(i) = p - .annotations - .collectFirst { case jsonField(name) => name } - .getOrElse(if (transformNames) nameTransform(p.label) else p.label) - aliasesBuilder ++= p - .annotations - .flatMap { - case jsonAliases(alias, aliases*) => (alias +: aliases).map(_ -> i) - case _ => Seq.empty - } + ctx.params.foreach { + var idx = 0 + p => + names(idx) = p + .annotations + .collectFirst { case jsonField(name) => name } + .getOrElse(if (transformNames) nameTransform(p.label) else p.label) + aliasesBuilder ++= p + .annotations + .flatMap { + case jsonAliases(alias, aliases*) => (alias +: aliases).map(_ -> idx) + case _ => Seq.empty + } + idx += 1 } val aliases = aliasesBuilder.result() - val allFieldNames = names ++ aliases.map(_._1) if (allFieldNames.length != allFieldNames.distinct.length) { val aliasNames = aliases.map(_._1) @@ -278,78 +278,73 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv s"alias(es) ${collisions.mkString(",")} collide with a field or another alias" throw new AssertionError(msg) } - (names, aliases) } - - val len: Int = names.length - val matrix: StringMatrix = new StringMatrix(names, aliases) - val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_)) - - lazy val tcs: Array[JsonDecoder[Any]] = + private val len = names.length + private val matrix = new StringMatrix(names, aliases) + private val spans = names.map(JsonError.ObjectAccess(_)) + private val defaults = IArray.genericWrapArray(ctx.params.map(_.default)).toArray + private lazy val tcs = IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] - - lazy val defaults: Array[Option[Any]] = - IArray.genericWrapArray(ctx.params.map(_.default)).toArray - - lazy val namesMap: Map[String, Int] = - (names.zipWithIndex ++ aliases).toMap + private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') val ps = new Array[Any](len) if (Lexer.firstField(trace, in)) while({ - val field = Lexer.field(trace, in, matrix) - if (field != -1) { - if (ps(field) != null) Lexer.error("duplicate", trace) - val default = defaults(field) - ps(field) = if ((default eq None) || in.nextNonWhitespace() != 'n' && { + val idx = Lexer.field(trace, in, matrix) + if (idx != -1) { + if (ps(idx) != null) Lexer.error("duplicate", trace) + val default = defaults(idx) + ps(idx) = if ((default eq None) || in.nextNonWhitespace() != 'n' && { in.retract() true - }) tcs(field).unsafeDecode(spans(field) :: trace, in) + }) tcs(idx).unsafeDecode(spans(idx) :: trace, in) else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get - else Lexer.error("expected 'null'", spans(field) :: trace) + else Lexer.error("expected 'null'", spans(idx) :: trace) } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) Lexer.nextField(trace, in) }) () - var i = 0 - while (i < len) { - if (ps(i) == null) { - ps(i) = - if (defaults(i) ne None) defaults(i).get - else tcs(i).unsafeDecodeMissing(spans(i) :: trace) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne None) default.get + else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } - i += 1 + idx += 1 } ctx.rawConstruct(new ArraySeq(ps)) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(fields) => + case Json.Obj(keyValues) => val ps = new Array[Any](len) - for ((key, value) <- fields) { + for ((key, value) <- keyValues) { namesMap.get(key) match { - case Some(field) => - if (ps(field) != null) Lexer.error("duplicate", trace) - ps(field) = { - if ((value eq Json.Null) && (defaults(field) ne None)) defaults(field).get - else tcs(field).unsafeFromJsonAST(spans(field) :: trace, value) - } + case Some(idx) => + if (ps(idx) != null) Lexer.error("duplicate", trace) + val default = defaults(idx) + ps(idx) = + if ((default ne None) && (value eq Json.Null)) default.get + else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, value) case _ => if (no_extra) Lexer.error("invalid extra field", trace) } } - var i = 0 - while (i < len) { - if (ps(i) == null) { - ps(i) = - if (defaults(i) ne None) defaults(i).get - else tcs(i).unsafeDecodeMissing(spans(i) :: trace) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne None) default.get + else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } - i += 1 + idx += 1 } ctx.rawConstruct(new ArraySeq(ps)) case _ => Lexer.error("Not an object", trace) @@ -362,61 +357,49 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val names: Array[String] = IArray.genericWrapArray(ctx.subtypes.map { p => - p.annotations.collectFirst { case jsonHint(name) => - name - }.getOrElse(jsonHintFormat(p.typeInfo.short)) + p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeInfo.short)) }).toArray - val matrix: StringMatrix = new StringMatrix(names) - lazy val tcs: Array[JsonDecoder[Any]] = IArray.genericWrapArray(ctx.subtypes.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] - - lazy val namesMap: Map[String, Int] = - names.zipWithIndex.toMap + lazy val namesMap: Map[String, Int] = names.zipWithIndex.toMap def isEnumeration = (ctx.isEnum && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[?, ?]])) || ( !ctx.isEnum && ctx.subtypes.forall(_.isObject) ) - def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) + def discrim = + ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) if (isEnumeration && discrim.isEmpty) { new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - val typeName = Lexer.string(trace, in).toString() - namesMap.find(_._1 == typeName) match { - case Some((_, idx)) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - case None => Lexer.error(s"Invalid enumeration value $typeName", trace) - } + val idx = Lexer.enumeration(trace, in, matrix) + if (idx != -1) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + else Lexer.error("Invalid enumeration value", trace) } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = { + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Str(typeName) => - ctx.subtypes.find(_.typeInfo.short == typeName) match { - case Some(sub) => sub.typeclass.asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - case None => Lexer.error(s"Invalid enumeration value $typeName", trace) - } + case Json.Str(typeName) => namesMap.get(typeName) match { + case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + case _ => Lexer.error("Invalid enumeration value", trace) + } case _ => Lexer.error("Not a string", trace) } - } } } else if (discrim.isEmpty) { // We're not allowing extra fields in this encoding new JsonDecoder[A] { - val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_)) + private val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_)) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') - if (Lexer.firstField(trace, in)) { - val field = Lexer.field(trace, in, matrix) - - if (field != -1) { - val trace_ = spans(field) :: trace - val a = tcs(field).unsafeDecode(trace_, in).asInstanceOf[A] + val idx = Lexer.field(trace, in, matrix) + if (idx != -1) { + val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] Lexer.char(trace, in, '}') a } else Lexer.error("invalid disambiguator", trace) @@ -426,10 +409,10 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = { json match { case Json.Obj(chunk) if chunk.size == 1 => - val (key, inner) = chunk.head - namesMap.get(key) match { - case Some(idx) => tcs(idx).unsafeFromJsonAST(JsonError.ObjectAccess(key) :: trace, inner).asInstanceOf[A] - case None => Lexer.error("Invalid disambiguator", trace) + val keyValue = chunk.head + namesMap.get(keyValue._1) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, keyValue._2).asInstanceOf[A] + case _ => Lexer.error("Invalid disambiguator", trace) } case Json.Obj(_) => Lexer.error("Not an object with a single field", trace) case _ => Lexer.error("Not an object", trace) @@ -438,9 +421,9 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } } else { new JsonDecoder[A] { - val hintfield = discrim.get - val hintmatrix = new StringMatrix(Array(hintfield)) - val spans: Array[JsonError] = names.map(JsonError.Message(_)) + private val hintfield = discrim.get + private val hintmatrix = new StringMatrix(Array(hintfield)) + private val spans = names.map(JsonError.Message(_)) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val in_ = zio.json.internal.RecordingReader(in) @@ -448,11 +431,10 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (Lexer.firstField(trace, in_)) { while ({ if (Lexer.field(trace, in_, hintmatrix) != -1) { - val field = Lexer.enumeration(trace, in_, matrix) - if (field == -1) Lexer.error("invalid disambiguator", trace) + val idx = Lexer.enumeration(trace, in_, matrix) + if (idx == -1) Lexer.error("invalid disambiguator", trace) in_.rewind() - val trace_ = spans(field) :: trace - return tcs(field).unsafeDecode(trace_, in_).asInstanceOf[A] + return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] } else Lexer.skipValue(trace, in_) Lexer.nextField(trace, in_) }) () @@ -463,16 +445,13 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = { json match { case Json.Obj(fields) => - fields.find { case (k, _) => k == hintfield } match { - case Some((_, Json.Str(name))) => - namesMap.get(name) match { - case Some(idx) => tcs(idx).unsafeFromJsonAST(JsonError.ObjectAccess(name) :: trace, json).asInstanceOf[A] - case None => Lexer.error("Invalid disambiguator", trace) - } - case Some(_) => - Lexer.error(s"Non-string hint '$hintfield'", trace) - case None => - Lexer.error(s"Missing hint '$hintfield'", trace) + fields.find { case (key, _) => key == hintfield } match { + case Some((_, Json.Str(name))) => namesMap.get(name) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] + case _ => Lexer.error("Invalid disambiguator", trace) + } + case Some(_) => Lexer.error(s"Non-string hint '$hintfield'", trace) + case _ => Lexer.error(s"Missing hint '$hintfield'", trace) } case _ => Lexer.error("Not an object", trace) } @@ -517,92 +496,66 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv caseObjectEncoder.narrow[A] } else { new JsonEncoder[A] { - val (transformNames, nameTransform): (Boolean, String => String) = - ctx.annotations.collectFirst { case jsonMemberNames(format) => format } - .orElse(Some(config.fieldNameMapping)) - .filter(_ != IdentityFormat) - .map(true -> _) - .getOrElse(false -> identity) - - val params = ctx - .params - .filterNot { param => - param - .annotations - .collectFirst { - case _: jsonExclude => () - } - .isDefined - } - - val len = params.length - - val names = - IArray.genericWrapArray(params - .map { p => - p.annotations.collectFirst { - case jsonField(name) => name - }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) - }) - .toArray - - val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - val explicitEmptyCollections = - ctx.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => - enabled - }.getOrElse(config.explicitEmptyCollections) - - lazy val tcs: Array[JsonEncoder[Any]] = - IArray.genericWrapArray(params.map(_.typeclass.asInstanceOf[JsonEncoder[Any]])).toArray + private val (transformNames, nameTransform): (Boolean, String => String) = ctx.annotations + .collectFirst { case jsonMemberNames(format) => format } + .orElse(Some(config.fieldNameMapping)) + .filter(_ != IdentityFormat) + .map(true -> _) + .getOrElse(false -> identity) + private val params = IArray.genericWrapArray(ctx.params.filterNot { param => + param.annotations.collectFirst { case _: jsonExclude => () }.isDefined + }).toArray + private val names = params.map { p => + p.annotations.collectFirst { + case jsonField(name) => name + }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) + }.toArray + private val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + private val explicitEmptyCollections = ctx.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => + enabled + }.getOrElse(config.explicitEmptyCollections) + private lazy val fields = params.map { + var idx = 0 + p => + val field = (p, names(idx), p.typeclass.asInstanceOf[JsonEncoder[Any]], + explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]), + p.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => + enabled + }.getOrElse(explicitEmptyCollections)) + idx += 1 + field + }.toArray def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { - out.write("{") - + out.write('{') var indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) - - var i = 0 + val fields = this.fields + var idx = 0 var prevFields = false - - while (i < len) { - val tc = tcs(i) - val p = params(i).deref(a) - val writeNulls = explicitNulls || params(i).annotations.exists(_.isInstanceOf[jsonExplicitNull]) - val writeEmptyCollections = - params(i).annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => - enabled - }.getOrElse(explicitEmptyCollections) - if ( - (!tc.isNothing(p) && !tc.isEmpty(p)) || (tc - .isNothing(p) && writeNulls) || (tc.isEmpty(p) && writeEmptyCollections) - ) { + while (idx < fields.length) { + val field = fields(idx) + val p = field._1.deref(a) + if ({ + val isNothing = field._3.isNothing(p) + val isEmpty = field._3.isEmpty(p) + (!isNothing && !isEmpty) || (isNothing && field._4) || (isEmpty && field._5) + }) { // if we have at least one field already, we need a comma if (prevFields) { - if (indent.isEmpty) { - out.write(",") - } else { - out.write(",") - JsonEncoder.pad(indent_, out) - } + out.write(',') + JsonEncoder.pad(indent_, out) } - - JsonEncoder.string.unsafeEncode(names(i), indent_, out) - - if (indent.isEmpty) { - out.write(":") - } else { - out.write(" : ") - } - - tc.unsafeEncode(p, indent_, out) + JsonEncoder.string.unsafeEncode(field._2, indent_, out) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + field._3.unsafeEncode(p, indent_, out) prevFields = true // at least one field so far } - - i += 1 + idx += 1 } - JsonEncoder.pad(indent, out) - out.write("}") + out.write('}') } override final def toJsonAST(a: A): Either[String, Json] = { @@ -636,10 +589,8 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv (ctx.isEnum && ctx.subtypes.forall(_.typeclass == caseObjectEncoder)) || ( !ctx.isEnum && ctx.subtypes.forall(_.isObject) ) - val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) - val discrim = ctx .annotations .collectFirst { @@ -683,22 +634,15 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv .collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - - out.write("{") + out.write('{') val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) JsonEncoder.string.unsafeEncode(name, indent_, out) - - if (indent.isEmpty) { - out.write(":") - } else { - out.write(" : ") - } - + if (indent.isEmpty) out.write(':') + else out.write(" : ") sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) JsonEncoder.pad(indent, out) - - out.write("}") + out.write('}') } } @@ -731,14 +675,13 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { ctx.choose(a) { sub => - out.write("{") + out.write('{') val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) JsonEncoder.string.unsafeEncode(hintField, indent_, out) - if (indent.isEmpty) out.write(":") + if (indent.isEmpty) out.write(':') else out.write(" : ") JsonEncoder.string.unsafeEncode(getName(sub.annotations, sub.typeInfo.short), indent_, out) - // whitespace is always off by 2 spaces at the end, probably not worth fixing val intermediate = new DeriveJsonEncoder.NestedWriter(out, indent_) sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) @@ -767,28 +710,49 @@ object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.de // intercepts the first `{` of a nested writer and discards it. We also need to // inject a `,` unless an empty object `{}` has been written. private[json] final class NestedWriter(out: Write, indent: Option[Int]) extends Write { - private[this] var first, second = true + private[this] var state = 2 + + def write(c: Char): Unit = + if (state != 0) { + if (c == ' ' || c == '\n') { + () + } else if (state == 2 && c == '{') { + state = 1 + } else if (state == 1) { + state = 0 + if (c != '}') { + out.write(',') + JsonEncoder.pad(indent, out) + } + out.write(c) + } + } else out.write(c) - def write(c: Char): Unit = write(c.toString) // could be optimised def write(s: String): Unit = - if (first || second) { + if (state != 0) { var i = 0 while (i < s.length) { val c = s.charAt(i) - if (c == ' ' || c == '\n') {} else if (first && c == '{') { - first = false - } else if (second) { - second = false + if (c == ' ' || c == '\n') { + () + } else if (state == 2 && c == '{') { + state = 1 + } else if (state == 1) { + state = 0 if (c != '}') { out.write(',') JsonEncoder.pad(indent, out) } - return out.write(s.substring(i)) + while (i < s.length) { + out.write(s.charAt(i)) + i += 1 + } + return } i += 1 } } else out.write(s) - } + } } object DeriveJsonCodec { diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index c7a0f09e5..9a0c237ac 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -369,18 +369,17 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with B: JsonDecoder[B] ): JsonDecoder[Either[A, B]] = new JsonDecoder[Either[A, B]] { - - val names: Array[String] = - Array("a", "Left", "left", "b", "Right", "right") - val matrix: StringMatrix = new StringMatrix(names) - val spans: Array[JsonError] = names.map(JsonError.ObjectAccess) + private[this] val names = Array("a", "Left", "left", "b", "Right", "right") + private[this] val matrix = new StringMatrix(names) + private[this] val spans = names.map(JsonError.ObjectAccess(_)) def unsafeDecode( trace: List[JsonError], in: RetractReader ): Either[A, B] = { Lexer.char(trace, in, '{') - val values = new Array[Any](2) + var left: Any = null + var right: Any = null if (Lexer.firstField(trace, in)) while ({ { @@ -389,19 +388,19 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with else { val trace_ = spans(field) :: trace if (field < 3) { - if (values(0) != null) Lexer.error("duplicate", trace_) - values(0) = A.unsafeDecode(trace_, in) + if (left != null) Lexer.error("duplicate", trace_) + left = A.unsafeDecode(trace_, in) } else { - if (values(1) != null) Lexer.error("duplicate", trace_) - values(1) = B.unsafeDecode(trace_, in) + if (right != null) Lexer.error("duplicate", trace_) + right = B.unsafeDecode(trace_, in) } } }; Lexer.nextField(trace, in) }) () - if (values(0) == null && values(1) == null) Lexer.error("missing fields", trace) - if (values(0) != null && values(1) != null) Lexer.error("ambiguous either, zip present", trace) - if (values(0) != null) Left(values(0).asInstanceOf[A]) - else Right(values(1).asInstanceOf[B]) + if (left == null && right == null) Lexer.error("missing fields", trace) + if (left != null && right != null) Lexer.error("ambiguous either, zip present", trace) + if (left != null) Left(left.asInstanceOf[A]) + else Right(right.asInstanceOf[B]) } } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala index 0426015b7..e117e1ebe 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala @@ -10,52 +10,48 @@ object DerivedDecoderSpec extends ZIOSpecDefault { test("Derives for a product type") { case class Foo(bar: String) derives JsonDecoder - val result = "{\"bar\": \"hello\"}".fromJson[Foo] - - assertTrue(result == Right(Foo("hello"))) + assertTrue("{\"bar\": \"hello\"}".fromJson[Foo] == Right(Foo("hello"))) }, test("Derives for a sum enum Enumeration type") { + @jsonHintNames(SnakeCase) enum Foo derives JsonDecoder: case Bar case Baz case Qux - val result = "\"Qux\"".fromJson[Foo] - - assertTrue(result == Right(Foo.Qux)) + assertTrue("\"qux\"".fromJson[Foo] == Right(Foo.Qux)) + assertTrue("\"bar\"".fromJson[Foo] == Right(Foo.Bar)) }, test("Derives for a sum sealed trait Enumeration type") { sealed trait Foo derives JsonDecoder object Foo: + @jsonHint("Barrr") case object Bar extends Foo case object Baz extends Foo case object Qux extends Foo - val result = "\"Qux\"".fromJson[Foo] - - assertTrue(result == Right(Foo.Qux)) + assertTrue("\"Qux\"".fromJson[Foo] == Right(Foo.Qux)) + assertTrue("\"Barrr\"".fromJson[Foo] == Right(Foo.Bar)) }, test("Derives for a sum sealed trait Enumeration type with discriminator") { @jsonDiscriminator("$type") sealed trait Foo derives JsonDecoder object Foo: + @jsonHint("Barrr") case object Bar extends Foo case object Baz extends Foo case object Qux extends Foo - val result = """{"$type":"Qux"}""".fromJson[Foo] - - assertTrue(result == Right(Foo.Qux)) + assertTrue("""{"$type":"Qux"}""".fromJson[Foo] == Right(Foo.Qux)) + assertTrue("""{"$type":"Barrr"}""".fromJson[Foo] == Right(Foo.Bar)) }, - test("Derives for a sum ADT type") { + test("Derives for a recursive sum ADT type") { enum Foo derives JsonDecoder: case Bar case Baz(baz: String) case Qux(foo: Foo) - val result = "{\"Qux\":{\"foo\":{\"Bar\":{}}}}".fromJson[Foo] - - assertTrue(result == Right(Foo.Qux(Foo.Bar))) + assertTrue("{\"Qux\":{\"foo\":{\"Bar\":{}}}}".fromJson[Foo] == Right(Foo.Qux(Foo.Bar))) }, test("Derives and decodes for a union of string-based literals") { case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonDecoder From 953f7ed971eb5d9bb10fc697afb3c0be35e1b290 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 19 Jan 2025 16:51:25 +0100 Subject: [PATCH 076/311] Fix traces of decoding error for tuples (#1225) --- build.sbt | 6 +++--- zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index fe67132b8..4f17c7b7a 100644 --- a/build.sbt +++ b/build.sbt @@ -138,18 +138,18 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) val tparams = (1 to i).map(p => s"A$p").mkString(", ") val implicits = (1 to i).map(p => s"A$p: JsonDecoder[A$p]").mkString(", ") val work = (1 to i) - .map(p => s"val a$p = A$p.unsafeDecode(trace :+ traces($p), in)") + .map(p => s"val a$p = A$p.unsafeDecode(traces(${p - 1}) :: trace, in)") .mkString("\n Lexer.char(trace, in, ',')\n ") val returns = (1 to i).map(p => s"a$p").mkString(", ") s"""implicit def tuple$i[$tparams](implicit $implicits): JsonDecoder[Tuple$i[$tparams]] = | new JsonDecoder[Tuple$i[$tparams]] { - | val traces: Array[JsonError] = (0 to $i).map(JsonError.ArrayAccess(_)).toArray + | private[this] val traces: Array[JsonError] = (0 to ${i - 1}).map(JsonError.ArrayAccess(_)).toArray | def unsafeDecode(trace: List[JsonError], in: RetractReader): Tuple$i[$tparams] = { | Lexer.char(trace, in, '[') | $work | Lexer.char(trace, in, ']') - | Tuple$i($returns) + | new Tuple$i($returns) | } | }""".stripMargin } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 55eb0d051..a2bfb3012 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -54,6 +54,13 @@ object DecoderSpec extends ZIOSpecDefault { forall(isRight(isRight(equalTo(2)))) ) }, + test("tuples") { + assert("""["a",3]""".fromJson[(String, Int)])(isRight(equalTo(("a", 3)))) + assert("""["a","b"]""".fromJson[(String, Int)])(isLeft(equalTo("[1](expected a number, got 'b')"))) + assert("""[[0.1,0.2],[0.3,0.4],[-0.3,-]]""".fromJson[Seq[(Double, Double)]])( + isLeft(equalTo("[2][1](expected a Double)")) + ) + }, test("parameterless products") { import exampleproducts._ From 774ebd68f08cd0c0654bdc513f9f491f20f4c609 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 20 Jan 2025 11:15:37 +0100 Subject: [PATCH 077/311] More efficient decoding of `UUID`, `Currency`, and `java.time._` values (#1231) --- .../src/main/scala/zio/json/JsonDecoder.scala | 106 +++++++++++------- .../scala/zio/json/javatime/parsers.scala | 3 +- .../main/scala/zio/json/uuid/UUIDParser.scala | 8 +- 3 files changed, 72 insertions(+), 45 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 9a0c237ac..cc02e24c1 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -445,14 +445,14 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = f(string.unsafeDecode(trace, in)) match { - case Left(err) => Lexer.error(err, trace) case Right(value) => value + case Left(err) => Lexer.error(err, trace) } override def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = f(string.unsafeFromJsonAST(trace, json)) match { - case Left(err) => Lexer.error(err, trace) case Right(value) => value + case Left(err) => Lexer.error(err, trace) } } } @@ -691,31 +691,42 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { import java.time.format.DateTimeParseException import java.time.zone.ZoneRulesException - implicit val dayOfWeek: JsonDecoder[DayOfWeek] = mapStringOrFail(s => parseJavaTime(DayOfWeek.valueOf, s.toUpperCase)) - implicit val duration: JsonDecoder[Duration] = mapStringOrFail(parseJavaTime(parsers.unsafeParseDuration, _)) - implicit val instant: JsonDecoder[Instant] = mapStringOrFail(parseJavaTime(parsers.unsafeParseInstant, _)) - implicit val localDate: JsonDecoder[LocalDate] = mapStringOrFail(parseJavaTime(parsers.unsafeParseLocalDate, _)) - - implicit val localDateTime: JsonDecoder[LocalDateTime] = - mapStringOrFail(parseJavaTime(parsers.unsafeParseLocalDateTime, _)) - - implicit val localTime: JsonDecoder[LocalTime] = mapStringOrFail(parseJavaTime(parsers.unsafeParseLocalTime, _)) - implicit val month: JsonDecoder[Month] = mapStringOrFail(s => parseJavaTime(Month.valueOf, s.toUpperCase)) - implicit val monthDay: JsonDecoder[MonthDay] = mapStringOrFail(parseJavaTime(parsers.unsafeParseMonthDay, _)) - - implicit val offsetDateTime: JsonDecoder[OffsetDateTime] = - mapStringOrFail(parseJavaTime(parsers.unsafeParseOffsetDateTime, _)) - - implicit val offsetTime: JsonDecoder[OffsetTime] = mapStringOrFail(parseJavaTime(parsers.unsafeParseOffsetTime, _)) - implicit val period: JsonDecoder[Period] = mapStringOrFail(parseJavaTime(parsers.unsafeParsePeriod, _)) - implicit val year: JsonDecoder[Year] = mapStringOrFail(parseJavaTime(parsers.unsafeParseYear, _)) - implicit val yearMonth: JsonDecoder[YearMonth] = mapStringOrFail(parseJavaTime(parsers.unsafeParseYearMonth, _)) - - implicit val zonedDateTime: JsonDecoder[ZonedDateTime] = - mapStringOrFail(parseJavaTime(parsers.unsafeParseZonedDateTime, _)) - - implicit val zoneId: JsonDecoder[ZoneId] = mapStringOrFail(parseJavaTime(parsers.unsafeParseZoneId, _)) - implicit val zoneOffset: JsonDecoder[ZoneOffset] = mapStringOrFail(parseJavaTime(parsers.unsafeParseZoneOffset, _)) + implicit val dayOfWeek: JsonDecoder[DayOfWeek] = javaTimeDecoder(s => DayOfWeek.valueOf(s.toUpperCase)) + implicit val duration: JsonDecoder[Duration] = javaTimeDecoder(parsers.unsafeParseDuration) + implicit val instant: JsonDecoder[Instant] = javaTimeDecoder(parsers.unsafeParseInstant) + implicit val localDate: JsonDecoder[LocalDate] = javaTimeDecoder(parsers.unsafeParseLocalDate) + implicit val localDateTime: JsonDecoder[LocalDateTime] = javaTimeDecoder(parsers.unsafeParseLocalDateTime) + implicit val localTime: JsonDecoder[LocalTime] = javaTimeDecoder(parsers.unsafeParseLocalTime) + implicit val month: JsonDecoder[Month] = javaTimeDecoder(s => Month.valueOf(s.toUpperCase)) + implicit val monthDay: JsonDecoder[MonthDay] = javaTimeDecoder(parsers.unsafeParseMonthDay) + implicit val offsetDateTime: JsonDecoder[OffsetDateTime] = javaTimeDecoder(parsers.unsafeParseOffsetDateTime) + implicit val offsetTime: JsonDecoder[OffsetTime] = javaTimeDecoder(parsers.unsafeParseOffsetTime) + implicit val period: JsonDecoder[Period] = javaTimeDecoder(parsers.unsafeParsePeriod) + implicit val year: JsonDecoder[Year] = javaTimeDecoder(parsers.unsafeParseYear) + implicit val yearMonth: JsonDecoder[YearMonth] = javaTimeDecoder(parsers.unsafeParseYearMonth) + implicit val zonedDateTime: JsonDecoder[ZonedDateTime] = javaTimeDecoder(parsers.unsafeParseZonedDateTime) + implicit val zoneId: JsonDecoder[ZoneId] = javaTimeDecoder(parsers.unsafeParseZoneId) + implicit val zoneOffset: JsonDecoder[ZoneOffset] = javaTimeDecoder(parsers.unsafeParseZoneOffset) + + private[this] def javaTimeDecoder[A](f: String => A): JsonDecoder[A] = new JsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = + parseJavaTime(trace, string.unsafeDecode(trace, in)) + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + parseJavaTime(trace, string.unsafeFromJsonAST(trace, json)) + + // Commonized handling for decoding from string to java.time Class + @inline + private[this] def parseJavaTime(trace: List[JsonError], s: String): A = + try f(s) + catch { + case zre: ZoneRulesException => Lexer.error(s"$s is not a valid ISO-8601 format, ${zre.getMessage}", trace) + case dtpe: DateTimeParseException => + Lexer.error(s"$s is not a valid ISO-8601 format, ${dtpe.getMessage}", trace) + case dte: DateTimeException => Lexer.error(s"$s is not a valid ISO-8601 format, ${dte.getMessage}", trace) + case ex: Exception => Lexer.error(ex.getMessage, trace) + } + } // Commonized handling for decoding from string to java.time Class private[json] def parseJavaTime[A](f: String => A, s: String): Either[String, A] = @@ -728,25 +739,38 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { case ex: Exception => Left(ex.getMessage) } - implicit val uuid: JsonDecoder[UUID] = - mapStringOrFail { str => - try { - Right(UUIDParser.unsafeParse(str)) - } catch { - case iae: IllegalArgumentException => Left(s"Invalid UUID: ${iae.getMessage}") + implicit val uuid: JsonDecoder[UUID] = new JsonDecoder[UUID] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): UUID = + parseUUID(trace, string.unsafeDecode(trace, in)) + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID = + parseUUID(trace, string.unsafeFromJsonAST(trace, json)) + + @inline + private[this] def parseUUID(trace: List[JsonError], s: String): UUID = + try UUIDParser.unsafeParse(s) + catch { + case iae: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${iae.getMessage}", trace) } - } + } + + implicit val currency: JsonDecoder[java.util.Currency] = new JsonDecoder[java.util.Currency] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): java.util.Currency = + parseCurrency(trace, string.unsafeDecode(trace, in)) + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.util.Currency = + parseCurrency(trace, string.unsafeFromJsonAST(trace, json)) - implicit val currency: JsonDecoder[java.util.Currency] = - mapStringOrFail { str => - try { - Right(java.util.Currency.getInstance(str)) - } catch { - case iae: IllegalArgumentException => Left(s"Invalid Currency: ${iae.getMessage}") + @inline + private[this] def parseCurrency(trace: List[JsonError], s: String): java.util.Currency = + try java.util.Currency.getInstance(s) + catch { + case iae: IllegalArgumentException => Lexer.error(s"Invalid Currency: ${iae.getMessage}", trace) } - } + } } private[json] trait DecoderLowPriority4 extends DecoderLowPriorityVersionSpecific { + @inline implicit def fromCodec[A](implicit codec: JsonCodec[A]): JsonDecoder[A] = codec.decoder } diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala index 026849d35..0325448e9 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala @@ -1572,6 +1572,7 @@ private[json] object parsers { private[this] def charError(ch: Char, pos: Int) = error(s"expected '$ch'", pos) - private[this] def error(msg: String, pos: Int) = + @noinline + private[this] def error(msg: String, pos: Int): Nothing = throw new DateTimeException(msg + " at index " + pos) with NoStackTrace } diff --git a/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala b/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala index b976fe265..a8ab572ac 100644 --- a/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala +++ b/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala @@ -16,6 +16,7 @@ package zio.json.uuid import scala.annotation.nowarn +import scala.util.control.NoStackTrace // A port of https://github.com/openjdk/jdk/commit/ebadfaeb2e1cc7b5ce5f101cd8a539bc5478cf5b with optimizations applied private[json] object UUIDParser { @@ -89,7 +90,7 @@ private[json] object UUIDParser { private[this] def unsafeParseExtended(input: String): java.util.UUID = { val len = input.length - if (len > 36) throw new IllegalArgumentException("UUID string too large") + if (len > 36) invalidUUIDError("UUID string too large") val dash1 = input.indexOf('-', 0) val dash2 = input.indexOf('-', dash1 + 1) val dash3 = input.indexOf('-', dash2 + 1) @@ -131,6 +132,7 @@ private[json] object UUIDParser { result } - private[this] def invalidUUIDError(input: String): IllegalArgumentException = - throw new IllegalArgumentException(input) + @noinline + private[this] def invalidUUIDError(input: String): Nothing = + throw new IllegalArgumentException(input) with NoStackTrace } From 0945fdadbf45512a4dc08f87b3c861c0ade435c4 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 20 Jan 2025 15:29:17 +0100 Subject: [PATCH 078/311] Strip invalid values in error messages (#1232) * Strip invalid values in error messages + more efficient decoding of `Char`, `java.util.UUID`, `java.util.Currency`, and `java.time._` values and `Int`, `Long`, and `java.util.UUID` keys * Remove redundant exception cases * Add missing tests for decoding --- .../src/main/scala/zio/json/JsonDecoder.scala | 87 +++++++++++-------- .../scala/zio/json/JsonFieldDecoder.scala | 42 ++++----- .../main/scala/zio/json/uuid/UUIDParser.scala | 14 +-- .../src/test/scala/zio/json/DecoderSpec.scala | 35 +++++++- .../test/scala/zio/json/JavaTimeSpec.scala | 12 +-- 5 files changed, 115 insertions(+), 75 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index cc02e24c1..4d104eefa 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -157,20 +157,20 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): B = f(self.unsafeDecode(trace, in)) match { - case Left(err) => Lexer.error(err, trace) case Right(b) => b + case Left(err) => Lexer.error(err, trace) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): B = f(self.unsafeFromJsonAST(trace, json)) match { - case Left(err) => Lexer.error(err, trace) case Right(b) => b + case Left(err) => Lexer.error(err, trace) } override def unsafeDecodeMissing(trace: List[JsonError]): B = f(self.unsafeDecodeMissing(trace)) match { - case Left(err) => Lexer.error(err, trace) case Right(b) => b + case Left(err) => Lexer.error(err, trace) } } @@ -271,7 +271,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): String = json match { case Json.Str(value) => value - case _ => Lexer.error("Not a string value", trace) + case _ => Lexer.error("expected string", trace) } } @@ -283,14 +283,24 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Boolean = json match { case Json.Bool(value) => value - case _ => Lexer.error("Not a bool value", trace) + case _ => Lexer.error("expected boolean", trace) } } - implicit val char: JsonDecoder[Char] = string.mapOrFail { - case str if str.length == 1 => Right(str(0)) - case _ => Left("expected one character") + implicit val char: JsonDecoder[Char] = new JsonDecoder[Char] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Char = { + val s = Lexer.string(trace, in) + if (s.length == 1) s.charAt(0) + else Lexer.error("expected single character string", trace) + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Char = + json match { + case Json.Str(s) if s.length == 1 => s.charAt(0) + case _ => Lexer.error("expected single character string", trace) + } } + implicit val symbol: JsonDecoder[Symbol] = string.map(Symbol(_)) implicit val byte: JsonDecoder[Byte] = number(Lexer.byte, _.byteValueExact()) @@ -481,10 +491,10 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def chunk[A: JsonDecoder]: JsonDecoder[Chunk[A]] = new JsonDecoder[Chunk[A]] { + private[this] val decoder = JsonDecoder[A] override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[A] = Chunk.empty - val decoder = JsonDecoder[A] def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[A] = builder(trace, in, zio.ChunkBuilder.make[A]()) @@ -688,8 +698,6 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { this: JsonDecoder.type => import java.time.{ DateTimeException, _ } - import java.time.format.DateTimeParseException - import java.time.zone.ZoneRulesException implicit val dayOfWeek: JsonDecoder[DayOfWeek] = javaTimeDecoder(s => DayOfWeek.valueOf(s.toUpperCase)) implicit val duration: JsonDecoder[Duration] = javaTimeDecoder(parsers.unsafeParseDuration) @@ -710,64 +718,75 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { private[this] def javaTimeDecoder[A](f: String => A): JsonDecoder[A] = new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = - parseJavaTime(trace, string.unsafeDecode(trace, in)) + parseJavaTime(trace, Lexer.string(trace, in).toString) - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - parseJavaTime(trace, string.unsafeFromJsonAST(trace, json)) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case Json.Str(value) => parseJavaTime(trace, value) + case _ => Lexer.error("expected string", trace) + } // Commonized handling for decoding from string to java.time Class @inline private[this] def parseJavaTime(trace: List[JsonError], s: String): A = try f(s) catch { - case zre: ZoneRulesException => Lexer.error(s"$s is not a valid ISO-8601 format, ${zre.getMessage}", trace) - case dtpe: DateTimeParseException => - Lexer.error(s"$s is not a valid ISO-8601 format, ${dtpe.getMessage}", trace) - case dte: DateTimeException => Lexer.error(s"$s is not a valid ISO-8601 format, ${dte.getMessage}", trace) - case ex: Exception => Lexer.error(ex.getMessage, trace) + case ex: DateTimeException => + Lexer.error(s"${strip(s)} is not a valid ISO-8601 format, ${ex.getMessage}", trace) + case _: IllegalArgumentException => + Lexer.error(s"${strip(s)} is not a valid ISO-8601 format", trace) } } // Commonized handling for decoding from string to java.time Class private[json] def parseJavaTime[A](f: String => A, s: String): Either[String, A] = - try { - Right(f(s)) - } catch { - case zre: ZoneRulesException => Left(s"$s is not a valid ISO-8601 format, ${zre.getMessage}") - case dtpe: DateTimeParseException => Left(s"$s is not a valid ISO-8601 format, ${dtpe.getMessage}") - case dte: DateTimeException => Left(s"$s is not a valid ISO-8601 format, ${dte.getMessage}") - case ex: Exception => Left(ex.getMessage) + try Right(f(s)) + catch { + case ex: DateTimeException => + Left(s"${strip(s)} is not a valid ISO-8601 format, ${ex.getMessage}") + case _: IllegalArgumentException => + Left(s"${strip(s)} is not a valid ISO-8601 format") } implicit val uuid: JsonDecoder[UUID] = new JsonDecoder[UUID] { def unsafeDecode(trace: List[JsonError], in: RetractReader): UUID = - parseUUID(trace, string.unsafeDecode(trace, in)) + parseUUID(trace, Lexer.string(trace, in).toString) - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID = - parseUUID(trace, string.unsafeFromJsonAST(trace, json)) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID = + json match { + case Json.Str(value) => parseUUID(trace, value) + case _ => Lexer.error("expected string", trace) + } @inline private[this] def parseUUID(trace: List[JsonError], s: String): UUID = try UUIDParser.unsafeParse(s) catch { - case iae: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${iae.getMessage}", trace) + case _: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${strip(s)}", trace) } } implicit val currency: JsonDecoder[java.util.Currency] = new JsonDecoder[java.util.Currency] { def unsafeDecode(trace: List[JsonError], in: RetractReader): java.util.Currency = - parseCurrency(trace, string.unsafeDecode(trace, in)) + parseCurrency(trace, Lexer.string(trace, in).toString) - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.util.Currency = - parseCurrency(trace, string.unsafeFromJsonAST(trace, json)) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.util.Currency = + json match { + case Json.Str(value) => parseCurrency(trace, value) + case _ => Lexer.error("expected string", trace) + } @inline private[this] def parseCurrency(trace: List[JsonError], s: String): java.util.Currency = try java.util.Currency.getInstance(s) catch { - case iae: IllegalArgumentException => Lexer.error(s"Invalid Currency: ${iae.getMessage}", trace) + case _: IllegalArgumentException => Lexer.error(s"Invalid Currency: ${strip(s)}", trace) } } + + private[json] def strip(s: String, len: Int = 50): String = + if (s.length <= len) s + else s.substring(0, len) + "..." } private[json] trait DecoderLowPriority4 extends DecoderLowPriorityVersionSpecific { diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala index 88148978a..f4336394a 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala @@ -49,30 +49,28 @@ object JsonFieldDecoder { def unsafeDecodeField(trace: List[JsonError], in: String): String = in } - implicit val int: JsonFieldDecoder[Int] = - JsonFieldDecoder[String].mapOrFail { str => - try { - Right(str.toInt) - } catch { - case n: NumberFormatException => Left(s"Invalid Int: '$str': $n") + implicit val int: JsonFieldDecoder[Int] = new JsonFieldDecoder[Int] { + def unsafeDecodeField(trace: List[JsonError], in: String): Int = + try in.toInt + catch { + case _: NumberFormatException => Lexer.error(s"Invalid Int: ${strip(in)}", trace) } - } + } - implicit val long: JsonFieldDecoder[Long] = - JsonFieldDecoder[String].mapOrFail { str => - try { - Right(str.toLong) - } catch { - case n: NumberFormatException => Left(s"Invalid Long: '$str': $n") + implicit val long: JsonFieldDecoder[Long] = new JsonFieldDecoder[Long] { + def unsafeDecodeField(trace: List[JsonError], in: String): Long = + try in.toLong + catch { + case _: NumberFormatException => Lexer.error(s"Invalid Long: ${strip(in)}", trace) } - } + } - implicit val uuid: JsonFieldDecoder[java.util.UUID] = mapStringOrFail { str => - try { - Right(UUIDParser.unsafeParse(str)) - } catch { - case iae: IllegalArgumentException => Left(s"Invalid UUID: ${iae.getMessage}") - } + implicit val uuid: JsonFieldDecoder[java.util.UUID] = new JsonFieldDecoder[java.util.UUID] { + def unsafeDecodeField(trace: List[JsonError], in: String): java.util.UUID = + try UUIDParser.unsafeParse(in) + catch { + case _: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${strip(in)}", trace) + } } // use this instead of `string.mapOrFail` in supertypes (to prevent class initialization error at runtime) @@ -84,4 +82,8 @@ object JsonFieldDecoder { case Right(value) => value } } + + private[json] def strip(s: String, len: Int = 50): String = + if (s.length <= len) s + else s.substring(0, len) + "..." } diff --git a/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala b/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala index a8ab572ac..c82120d21 100644 --- a/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala +++ b/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala @@ -73,7 +73,7 @@ private[json] object UUIDParser { val lsb2 = parseNibbles(ch2n, input, 24) val lsb3 = parseNibbles(ch2n, input, 28) val lsb4 = parseNibbles(ch2n, input, 32) - if ((msb1 | msb2 | msb3 | msb4 | lsb1 | lsb2 | lsb3 | lsb4) < 0) invalidUUIDError(input) + if ((msb1 | msb2 | msb3 | msb4 | lsb1 | lsb2 | lsb3 | lsb4) < 0) invalidUUIDError() new java.util.UUID(msb1 << 48 | msb2 << 32 | msb3 << 16 | msb4, lsb1 << 48 | lsb2 << 32 | lsb3 << 16 | lsb4) } @@ -90,7 +90,7 @@ private[json] object UUIDParser { private[this] def unsafeParseExtended(input: String): java.util.UUID = { val len = input.length - if (len > 36) invalidUUIDError("UUID string too large") + if (len > 36) invalidUUIDError() val dash1 = input.indexOf('-', 0) val dash2 = input.indexOf('-', dash1 + 1) val dash3 = input.indexOf('-', dash2 + 1) @@ -102,7 +102,7 @@ private[json] object UUIDParser { // - if dash1 is -1, dash4 will be -1 // - if dash1 is positive but dash2 is -1, dash4 will be -1 // - if dash1 and dash2 is positive, dash3 will be -1, dash4 will be positive, but so will dash5 - if (dash4 < 0 || dash5 >= 0) invalidUUIDError(input) + if (dash4 < 0 || dash5 >= 0) invalidUUIDError() val ch2n = CharToNumeric val section1 = parseSection(ch2n, input, 0, dash1, 0xfffffff00000000L) @@ -121,18 +121,18 @@ private[json] object UUIDParser { endIndex: Int, zeroMask: Long ): Long = { - if (beginIndex >= endIndex || beginIndex + 16 < endIndex) invalidUUIDError(input) + if (beginIndex >= endIndex || beginIndex + 16 < endIndex) invalidUUIDError() var result = 0L var i = beginIndex while (i < endIndex) { result = (result << 4) | ch2n(input.charAt(i)) i += 1 } - if ((result & zeroMask) != 0) invalidUUIDError(input) + if ((result & zeroMask) != 0) invalidUUIDError() result } @noinline - private[this] def invalidUUIDError(input: String): Nothing = - throw new IllegalArgumentException(input) with NoStackTrace + private[this] def invalidUUIDError(): Nothing = + throw new IllegalArgumentException with NoStackTrace } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index a2bfb3012..3d1ab308f 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -256,6 +256,18 @@ object DecoderSpec extends ZIOSpecDefault { val jsonStr = JsonEncoder[Map[String, String]].encodeJson(expected, None) assert(jsonStr.fromJson[Map[String, String]])(isRight(equalTo(expected))) }, + test("Map with Int keys") { + assert("""{"1234567890": "value"}""".fromJson[Map[Int, String]])( + isRight(equalTo(Map(1234567890 -> "value"))) + ) && + assert("""{"xxx": "value"}""".fromJson[Map[Int, String]])(isLeft(containsString("Invalid Int: xxx"))) + }, + test("Map with Long keys") { + assert("""{"1234567890123456789": "value"}""".fromJson[Map[Long, String]])( + isRight(equalTo(Map(1234567890123456789L -> "value"))) + ) && + assert("""{"xxx": "value"}""".fromJson[Map[Long, String]])(isLeft(containsString("Invalid Long: xxx"))) + }, test("Map with UUID keys") { def expectedMap(str: String): Map[UUID, String] = Map(UUID.fromString(str) -> "value") @@ -280,7 +292,9 @@ object DecoderSpec extends ZIOSpecDefault { isRight(equalTo(expectedMap("00000000-0000-0000-0000-000000000000"))) ) && assert(bad1.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: "))) && - assert(bad2.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: UUID string too large"))) && + assert(bad2.fromJson[Map[UUID, String]])( + isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-9832-4e70afe4b0f80")) + ) && assert(bad3.fromJson[Map[UUID, String]])( isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80")) ) && @@ -328,7 +342,7 @@ object DecoderSpec extends ZIOSpecDefault { assert(ok2.fromJson[UUID])(isRight(equalTo(UUID.fromString("64D7C38D-00FD-0014-0032-0070AfE4B0f8")))) && assert(ok3.fromJson[UUID])(isRight(equalTo(UUID.fromString("00000000-0000-0000-0000-000000000000")))) && assert(bad1.fromJson[UUID])(isLeft(containsString("Invalid UUID: "))) && - assert(bad2.fromJson[UUID])(isLeft(containsString("Invalid UUID: UUID string too large"))) && + assert(bad2.fromJson[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-9832-4e70afe4b0f80"))) && assert(bad3.fromJson[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80"))) && assert(bad4.fromJson[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8"))) && assert(bad5.fromJson[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"))) && @@ -375,6 +389,19 @@ object DecoderSpec extends ZIOSpecDefault { test("BigDecimal") { assert(Json.Num(123).as[BigDecimal])(isRight(equalTo(BigDecimal(123)))) }, + test("boolean") { + assert(Json.Bool(true).as[Boolean])(isRight(equalTo(true))) && + assert(Json.Str("true").as[Boolean])(isLeft(equalTo("(expected boolean)"))) + }, + test("string") { + assert(Json.Str("xxx").as[String])(isRight(equalTo("xxx"))) && + assert(Json.Bool(true).as[String])(isLeft(equalTo("(expected string)"))) + }, + test("char") { + assert(Json.Str("x").as[Char])(isRight(equalTo('x'))) && + assert(Json.Str("xxx").as[Char])(isLeft(equalTo("(expected single character string)"))) && + assert(Json.Bool(true).as[Char])(isLeft(equalTo("(expected single character string)"))) + }, test("eithers") { val bernies = List(Json.Obj("a" -> Json.Num(1)), Json.Obj("left" -> Json.Num(1)), Json.Obj("Left" -> Json.Num(1))) @@ -405,7 +432,7 @@ object DecoderSpec extends ZIOSpecDefault { import exampleproducts._ assert(Json.Obj("is" -> Json.Arr(Json.Obj("str" -> Json.Num(1)))).as[Outer])( - isLeft(equalTo(".is[0].str(Not a string value)")) + isLeft(equalTo(".is[0].str(expected string)")) ) }, test("default field value") { @@ -549,7 +576,7 @@ object DecoderSpec extends ZIOSpecDefault { assert(ok2.as[UUID])(isRight(equalTo(UUID.fromString("64D7C38D-00FD-0014-0032-0070AFE4B0f8")))) && assert(ok3.as[UUID])(isRight(equalTo(UUID.fromString("00000000-0000-0000-0000-000000000000")))) && assert(bad1.as[UUID])(isLeft(containsString("Invalid UUID: "))) && - assert(bad2.as[UUID])(isLeft(containsString("Invalid UUID: UUID string too large"))) && + assert(bad2.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-9832-4e70afe4b0f80"))) && assert(bad3.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80"))) && assert(bad4.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8"))) && assert(bad5.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"))) && diff --git a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala index 3ef3584a0..943a3d16f 100644 --- a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala @@ -342,11 +342,7 @@ object JavaTimeSpec extends ZIOSpecDefault { suite("Decoder Sad Path")( test("DayOfWeek") { assert(stringify("foody").fromJson[DayOfWeek])( - isLeft( - equalTo("(No enum constant java.time.DayOfWeek.FOODY)") || // JVM - equalTo("(Unrecognized day of week name: FOODY)") || // Scala.js 2. - equalTo("(enum case not found: FOODY)") // Scala.js 3. - ) + isLeft(equalTo("(foody is not a valid ISO-8601 format)")) ) }, test("Duration") { @@ -1496,11 +1492,7 @@ object JavaTimeSpec extends ZIOSpecDefault { }, test("Month") { assert(stringify("FebTober").fromJson[Month])( - isLeft( - equalTo("(No enum constant java.time.Month.FEBTOBER)") || // JVM - equalTo("(Unrecognized month name: FEBTOBER)") || // Scala.js 2. - equalTo("(enum case not found: FEBTOBER)") // Scala.js 3. - ) + isLeft(equalTo("(FebTober is not a valid ISO-8601 format)")) ) }, test("MonthDay") { From 681aef732fe0929cccc805f053721160ab56dc01 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 20 Jan 2025 17:35:55 +0100 Subject: [PATCH 079/311] Avoid appending JSON values to error messages (#1233) --- .../src/main/scala/zio/json/ast/JsonType.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/JsonType.scala b/zio-json/shared/src/main/scala/zio/json/ast/JsonType.scala index 5ce900564..7d26596a5 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/JsonType.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/JsonType.scala @@ -24,7 +24,7 @@ object JsonType { def get(json: Json): Either[String, Json.Null] = json match { case Json.Null => Right(Json.Null) - case _ => Left("Expected null but found " + json) + case _ => Left("expected null") } } @@ -32,7 +32,7 @@ object JsonType { def get(json: Json): Either[String, Json.Bool] = json match { case x @ Json.Bool(_) => Right(x) - case _ => Left("Expected boolean but found " + json) + case _ => Left("expected boolean") } } @@ -40,7 +40,7 @@ object JsonType { def get(json: Json): Either[String, Json.Obj] = json match { case x @ Json.Obj(_) => Right(x) - case _ => Left("Expected object but found " + json) + case _ => Left("expected object") } } @@ -48,7 +48,7 @@ object JsonType { def get(json: Json): Either[String, Json.Arr] = json match { case x @ Json.Arr(_) => Right(x) - case _ => Left("Expected array but found " + json) + case _ => Left("expected array") } } @@ -56,7 +56,7 @@ object JsonType { def get(json: Json): Either[String, Json.Str] = json match { case x @ Json.Str(_) => Right(x) - case _ => Left("Expected string but found " + json) + case _ => Left("expected string") } } @@ -64,7 +64,7 @@ object JsonType { def get(json: Json): Either[String, Json.Num] = json match { case x @ Json.Num(_) => Right(x) - case _ => Left("Expected number but found " + json) + case _ => Left("expected number") } } } From 6de0c926f0fea8a13bafac4a3e675bf95e560e8b Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 20 Jan 2025 18:46:57 +0100 Subject: [PATCH 080/311] Use a fast path for more efficient encoding of strings that do not require encoding (#1234) --- .../src/main/scala/zio/json/JsonEncoder.scala | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 86b65e0d8..ca09d2977 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -120,8 +120,26 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def unsafeEncode(a: String, indent: Option[Int], out: Write): Unit = { out.write('"') + val len = a.length var i = 0 + while (i < len) { + val c = a.charAt(i) + i += 1 + if (c == '"' || c == '\\' || c < ' ') { + writeEncoded(a, out) + return + } + } + out.write(a) + out.write('"') + } + + override final def toJsonAST(a: String): Either[String, Json] = + Right(Json.Str(a)) + + private[this] def writeEncoded(a: String, out: Write): Unit = { val len = a.length + var i = 0 while (i < len) { (a.charAt(i): @switch) match { case '"' => out.write("\\\"") @@ -140,8 +158,6 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: String): Either[String, Json] = - Right(Json.Str(a)) } implicit val char: JsonEncoder[Char] = new JsonEncoder[Char] { @@ -151,6 +167,11 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with (a: @switch) match { case '"' => out.write("\\\"") case '\\' => out.write("\\\\") + case '\b' => out.write("\\b") + case '\f' => out.write("\\f") + case '\n' => out.write("\\n") + case '\r' => out.write("\\r") + case '\t' => out.write("\\t") case c => if (c < ' ') out.write("\\u%04x".format(c.toInt)) else out.write(c) From cdbb36fb1065619c1702d012c6abea91760045e0 Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:40:05 +0100 Subject: [PATCH 081/311] revert feature excludeEmptyCollections, remove completely (#1236) --- project/BuildHelper.scala | 2 +- .../src/main/scala-2.x/zio/json/macros.scala | 33 +- .../src/main/scala-3/zio/json/macros.scala | 34 +- .../zio/json/JsonCodecConfiguration.scala | 54 +- .../src/main/scala/zio/json/JsonDecoder.scala | 158 ++---- .../src/main/scala/zio/json/JsonEncoder.scala | 33 +- .../scala/zio/json/AnnotationsCodecSpec.scala | 407 -------------- .../json/ConfigurableDeriveCodecSpec.scala | 507 ++---------------- 8 files changed, 122 insertions(+), 1106 deletions(-) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 106fe18c8..5b8643048 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -244,7 +244,7 @@ object BuildHelper { autoAPIMappings := true, unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library"), mimaPreviousArtifacts := { - previousStableVersion.value.map(organization.value %% name.value % _).toSet ++ + previousStableVersion.value.filter(_ != "0.7.4").map(organization.value %% name.value % _).toSet ++ Set(organization.value %% name.value % "0.7.3") }, mimaCheckDirection := "backward", // TODO: find how we can use "both" for path versions diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index bc451f495..7ec5366bd 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -19,12 +19,10 @@ final case class jsonField(name: String) extends Annotation */ final case class jsonAliases(alias: String, aliases: String*) extends Annotation -final class jsonExplicitNull extends Annotation - /** - * When disabled keys with empty collections will be omitted from the JSON. + * Empty option fields will be encoded as `null`. */ -final case class jsonExplicitEmptyCollection(enabled: Boolean = true) extends Annotation +final class jsonExplicitNull extends Annotation /** * If used on a sealed class, will determine the name of the field for disambiguating classes. @@ -214,6 +212,7 @@ object DeriveJsonDecoder { if (ctx.parameters.isEmpty) new JsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { if (no_extra) { Lexer.char(trace, in, '{') @@ -268,7 +267,7 @@ object DeriveJsonDecoder { ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') // TODO it would be more efficient to have a solution that didn't box @@ -433,6 +432,7 @@ object DeriveJsonEncoder { def join[A](ctx: CaseClass[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = if (ctx.parameters.isEmpty) new JsonEncoder[A] { + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = out.write("{}") override final def toJsonAST(a: A): Either[String, Json] = @@ -457,10 +457,6 @@ object DeriveJsonEncoder { } private[this] val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - private[this] val explicitEmptyCollections = - ctx.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => - enabled - }.getOrElse(config.explicitEmptyCollections) private[this] lazy val fields = params.map { var idx = 0 p => @@ -468,17 +464,12 @@ object DeriveJsonEncoder { p, names(idx), p.typeclass.asInstanceOf[JsonEncoder[Any]], - explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]), - p.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => - enabled - }.getOrElse(explicitEmptyCollections) + explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) ) idx += 1 field } - override def isEmpty(a: A): Boolean = params.forall(p => p.typeclass.isEmpty(p.dereference(a))) - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { out.write('{') val indent_ = JsonEncoder.bump(indent) @@ -491,8 +482,7 @@ object DeriveJsonEncoder { val p = field._1.dereference(a) if ({ val isNothing = field._3.isNothing(p) - val isEmpty = field._3.isEmpty(p) - (!isNothing && !isEmpty) || (isNothing && field._4) || (isEmpty && field._5) + !isNothing || field._4 }) { // if we have at least one field already, we need a comma if (prevFields) { @@ -518,16 +508,9 @@ object DeriveJsonEncoder { name }.getOrElse(nameTransform(param.label)) val writeNulls = explicitNulls || param.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - val writeEmptyCollections = - param.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => - enabled - }.getOrElse(explicitEmptyCollections) c.flatMap { chunk => param.typeclass.toJsonAST(param.dereference(a)).map { value => - if ( - (value == Json.Null && !writeNulls) || - (value.asObject.exists(_.fields.isEmpty) && !writeEmptyCollections) - ) chunk + if (!writeNulls && value == Json.Null) chunk else chunk :+ name -> value } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 4ab9af2c8..e7f8ae583 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -32,11 +32,6 @@ final case class jsonAliases(alias: String, aliases: String*) extends Annotation */ final class jsonExplicitNull extends Annotation -/** - * When disabled keys with empty collections will be omitted from the JSON. - */ -final case class jsonExplicitEmptyCollection(enabled: Boolean = true) extends Annotation - /** * If used on a sealed class, will determine the name of the field for * disambiguating classes. @@ -288,7 +283,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') val ps = new Array[Any](len) if (Lexer.firstField(trace, in)) @@ -470,6 +465,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } private lazy val caseObjectEncoder = new JsonEncoder[Any] { + def unsafeEncode(a: Any, indent: Option[Int], out: Write): Unit = out.write("{}") @@ -511,17 +507,15 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) }.toArray private val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - private val explicitEmptyCollections = ctx.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => - enabled - }.getOrElse(config.explicitEmptyCollections) private lazy val fields = params.map { var idx = 0 p => - val field = (p, names(idx), p.typeclass.asInstanceOf[JsonEncoder[Any]], - explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]), - p.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => - enabled - }.getOrElse(explicitEmptyCollections)) + val field = ( + p, + names(idx), + p.typeclass.asInstanceOf[JsonEncoder[Any]], + explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + ) idx += 1 field }.toArray @@ -538,8 +532,7 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv val p = field._1.deref(a) if ({ val isNothing = field._3.isNothing(p) - val isEmpty = field._3.isEmpty(p) - (!isNothing && !isEmpty) || (isNothing && field._4) || (isEmpty && field._5) + !isNothing || field._4 }) { // if we have at least one field already, we need a comma if (prevFields) { @@ -565,16 +558,9 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv name }.getOrElse(nameTransform(param.label)) val writeNulls = explicitNulls || param.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - val writeEmptyCollections = - param.annotations.collectFirst { case jsonExplicitEmptyCollection(enabled) => - enabled - }.getOrElse(explicitEmptyCollections) c.flatMap { chunk => param.typeclass.toJsonAST(param.deref(a)).map { value => - if ( - (value == Json.Null && !writeNulls) || - (value.asObject.exists(_.fields.isEmpty) && !writeEmptyCollections) - ) chunk + if (!writeNulls && value == Json.Null) chunk else chunk :+ name -> value } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala index 3c82e8125..9f8428d18 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala @@ -14,77 +14,33 @@ import zio.json.JsonCodecConfiguration.SumTypeHandling.WrapperWithClassNameField * see [[jsonNoExtraFields]] * @param sumTypeMapping * see [[jsonHintNames]] + * @param explicitNulls + * see [[jsonExplicitNull]] */ final case class JsonCodecConfiguration( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, fieldNameMapping: JsonMemberFormat = IdentityFormat, allowExtraFields: Boolean = true, sumTypeMapping: JsonMemberFormat = IdentityFormat, - explicitNulls: Boolean = false, - explicitEmptyCollections: Boolean = true + explicitNulls: Boolean = false ) { - def this( - sumTypeHandling: SumTypeHandling, - fieldNameMapping: JsonMemberFormat, - allowExtraFields: Boolean, - sumTypeMapping: JsonMemberFormat, - explicitNulls: Boolean - ) = this( - sumTypeHandling, - fieldNameMapping, - allowExtraFields, - sumTypeMapping, - explicitNulls, - true - ) def copy( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField.asInstanceOf[SumTypeHandling], fieldNameMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], allowExtraFields: Boolean = true, sumTypeMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], - explicitNulls: Boolean = false, - explicitEmptyCollections: Boolean = true + explicitNulls: Boolean = false ) = new JsonCodecConfiguration( sumTypeHandling, fieldNameMapping, allowExtraFields, sumTypeMapping, - explicitNulls, - explicitEmptyCollections - ) - - def copy( - sumTypeHandling: SumTypeHandling, - fieldNameMapping: JsonMemberFormat, - allowExtraFields: Boolean, - sumTypeMapping: JsonMemberFormat, - explicitNulls: Boolean - ) = new JsonCodecConfiguration( - sumTypeHandling, - fieldNameMapping, - allowExtraFields, - sumTypeMapping, - explicitNulls, - true + explicitNulls ) } object JsonCodecConfiguration { - def apply( - sumTypeHandling: SumTypeHandling, - fieldNameMapping: JsonMemberFormat, - allowExtraFields: Boolean, - sumTypeMapping: JsonMemberFormat, - explicitNulls: Boolean - ) = new JsonCodecConfiguration( - sumTypeHandling, - fieldNameMapping, - allowExtraFields, - sumTypeMapping, - explicitNulls, - true - ) implicit val default: JsonCodecConfiguration = JsonCodecConfiguration() diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 4d104eefa..2ca5c4bfb 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -351,6 +351,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with // // If alternative behaviour is desired, e.g. pass null to the underlying, then // use a newtype wrapper. + implicit def option[A](implicit A: JsonDecoder[A]): JsonDecoder[Option[A]] = new JsonDecoder[Option[A]] { self => override def unsafeDecodeMissing(trace: List[JsonError]): Option[A] = None @@ -470,47 +471,36 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { this: JsonDecoder.type => - implicit def array[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[Array[A]] = - new JsonDecoder[Array[A]] { - - override def unsafeDecodeMissing(trace: List[JsonError]): Array[A] = Array.empty - - def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = - builder(trace, in, Array.newBuilder[A]) - } - - implicit def seq[A: JsonDecoder]: JsonDecoder[Seq[A]] = - new JsonDecoder[Seq[A]] { - - override def unsafeDecodeMissing(trace: List[JsonError]): Seq[A] = - Seq.empty + implicit def array[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[Array[A]] = new JsonDecoder[Array[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Seq[A] = - builder(trace, in, immutable.Seq.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = + builder(trace, in, Array.newBuilder[A]) + } - implicit def chunk[A: JsonDecoder]: JsonDecoder[Chunk[A]] = - new JsonDecoder[Chunk[A]] { - private[this] val decoder = JsonDecoder[A] + implicit def seq[A: JsonDecoder]: JsonDecoder[Seq[A]] = new JsonDecoder[Seq[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[A] = Chunk.empty + def unsafeDecode(trace: List[JsonError], in: RetractReader): Seq[A] = + builder(trace, in, immutable.Seq.newBuilder[A]) + } - def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[A] = - builder(trace, in, zio.ChunkBuilder.make[A]()) + implicit def chunk[A: JsonDecoder]: JsonDecoder[Chunk[A]] = new JsonDecoder[Chunk[A]] { + private[this] val decoder = JsonDecoder[A] + def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[A] = + builder(trace, in, zio.ChunkBuilder.make[A]()) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = - json match { - case Json.Arr(elements) => - elements.map { - var i = 0 - json => - val span = JsonError.ArrayAccess(i) - i += 1 - decoder.unsafeFromJsonAST(span :: trace, json) - } - case _ => Lexer.error("Not an array", trace) - } - } + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = + json match { + case Json.Arr(elements) => + elements.map { + var i = 0 + json => + val span = JsonError.ArrayAccess(i) + i += 1 + decoder.unsafeFromJsonAST(span :: trace, json) + } + case _ => Lexer.error("Not an array", trace) + } + } implicit def nonEmptyChunk[A: JsonDecoder]: JsonDecoder[NonEmptyChunk[A]] = chunk[A].mapOrFail(NonEmptyChunk.fromChunk(_).toRight("Chunk was empty")) @@ -518,8 +508,6 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def indexedSeq[A: JsonDecoder]: JsonDecoder[IndexedSeq[A]] = new JsonDecoder[IndexedSeq[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): IndexedSeq[A] = IndexedSeq.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): IndexedSeq[A] = builder(trace, in, IndexedSeq.newBuilder[A]) } @@ -527,76 +515,50 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def linearSeq[A: JsonDecoder]: JsonDecoder[immutable.LinearSeq[A]] = new JsonDecoder[immutable.LinearSeq[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.LinearSeq[A] = - immutable.LinearSeq.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): LinearSeq[A] = builder(trace, in, immutable.LinearSeq.newBuilder[A]) } - implicit def listSet[A: JsonDecoder]: JsonDecoder[immutable.ListSet[A]] = - new JsonDecoder[immutable.ListSet[A]] { - - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListSet[A] = - immutable.ListSet.empty + implicit def listSet[A: JsonDecoder]: JsonDecoder[immutable.ListSet[A]] = new JsonDecoder[immutable.ListSet[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): ListSet[A] = - builder(trace, in, immutable.ListSet.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): ListSet[A] = + builder(trace, in, immutable.ListSet.newBuilder[A]) + } implicit def treeSet[A: JsonDecoder: Ordering]: JsonDecoder[immutable.TreeSet[A]] = new JsonDecoder[immutable.TreeSet[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.TreeSet[A] = - immutable.TreeSet.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): TreeSet[A] = builder(trace, in, immutable.TreeSet.newBuilder[A]) } - implicit def list[A: JsonDecoder]: JsonDecoder[List[A]] = - new JsonDecoder[List[A]] { + implicit def list[A: JsonDecoder]: JsonDecoder[List[A]] = new JsonDecoder[List[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): List[A] = List.empty - - def unsafeDecode(trace: List[JsonError], in: RetractReader): List[A] = - builder(trace, in, new mutable.ListBuffer[A]) - } - - implicit def vector[A: JsonDecoder]: JsonDecoder[Vector[A]] = - new JsonDecoder[Vector[A]] { - - override def unsafeDecodeMissing(trace: List[JsonError]): Vector[A] = - Vector.empty - - def unsafeDecode(trace: List[JsonError], in: RetractReader): Vector[A] = - builder(trace, in, immutable.Vector.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): List[A] = + builder(trace, in, new mutable.ListBuffer[A]) + } - implicit def set[A: JsonDecoder]: JsonDecoder[Set[A]] = - new JsonDecoder[Set[A]] { + implicit def vector[A: JsonDecoder]: JsonDecoder[Vector[A]] = new JsonDecoder[Vector[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Set[A] = Set.empty + def unsafeDecode(trace: List[JsonError], in: RetractReader): Vector[A] = + builder(trace, in, immutable.Vector.newBuilder[A]) + } - def unsafeDecode(trace: List[JsonError], in: RetractReader): Set[A] = - builder(trace, in, Set.newBuilder[A]) - } + implicit def set[A: JsonDecoder]: JsonDecoder[Set[A]] = new JsonDecoder[Set[A]] { - implicit def hashSet[A: JsonDecoder]: JsonDecoder[immutable.HashSet[A]] = - new JsonDecoder[immutable.HashSet[A]] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Set[A] = + builder(trace, in, Set.newBuilder[A]) + } - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashSet[A] = - immutable.HashSet.empty + implicit def hashSet[A: JsonDecoder]: JsonDecoder[immutable.HashSet[A]] = new JsonDecoder[immutable.HashSet[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashSet[A] = - builder(trace, in, immutable.HashSet.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashSet[A] = + builder(trace, in, immutable.HashSet.newBuilder[A]) + } implicit def map[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[Map[K, V]] = new JsonDecoder[Map[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Map[K, V] = Map.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): Map[K, V] = keyValueBuilder(trace, in, Map.newBuilder[K, V]) } @@ -604,9 +566,6 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def hashMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.HashMap[K, V]] = new JsonDecoder[immutable.HashMap[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashMap[K, V] = - immutable.HashMap.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashMap[K, V] = keyValueBuilder(trace, in, immutable.HashMap.newBuilder[K, V]) } @@ -614,8 +573,6 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def mutableMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[mutable.Map[K, V]] = new JsonDecoder[mutable.Map[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): mutable.Map[K, V] = mutable.Map.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): mutable.Map[K, V] = keyValueBuilder(trace, in, mutable.Map.newBuilder[K, V]) } @@ -623,9 +580,6 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def sortedSet[A: Ordering: JsonDecoder]: JsonDecoder[immutable.SortedSet[A]] = new JsonDecoder[immutable.SortedSet[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.SortedSet[A] = - immutable.SortedSet.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.SortedSet[A] = builder(trace, in, immutable.SortedSet.newBuilder[A]) } @@ -633,9 +587,6 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def sortedMap[K: JsonFieldDecoder: Ordering, V: JsonDecoder]: JsonDecoder[collection.SortedMap[K, V]] = new JsonDecoder[collection.SortedMap[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): collection.SortedMap[K, V] = - collection.SortedMap.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): collection.SortedMap[K, V] = keyValueBuilder(trace, in, collection.SortedMap.newBuilder[K, V]) } @@ -643,9 +594,6 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def listMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.ListMap[K, V]] = new JsonDecoder[immutable.ListMap[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListMap[K, V] = - immutable.ListMap.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.ListMap[K, V] = keyValueBuilder(trace, in, immutable.ListMap.newBuilder[K, V]) } @@ -665,14 +613,11 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { private[json] trait DecoderLowPriority2 extends DecoderLowPriority3 { this: JsonDecoder.type => - implicit def iterable[A: JsonDecoder]: JsonDecoder[Iterable[A]] = - new JsonDecoder[Iterable[A]] { - - override def unsafeDecodeMissing(trace: List[JsonError]): Iterable[A] = Iterable.empty + implicit def iterable[A: JsonDecoder]: JsonDecoder[Iterable[A]] = new JsonDecoder[Iterable[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Iterable[A] = - builder(trace, in, immutable.Iterable.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Iterable[A] = + builder(trace, in, immutable.Iterable.newBuilder[A]) + } // not implicit because this overlaps with decoders for lists of tuples def keyValueChunk[K, A](implicit @@ -681,9 +626,6 @@ private[json] trait DecoderLowPriority2 extends DecoderLowPriority3 { ): JsonDecoder[Chunk[(K, A)]] = new JsonDecoder[Chunk[(K, A)]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[(K, A)] = - Chunk.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[(K, A)] = keyValueBuilder[K, A, ({ type lambda[X, Y] = Chunk[(X, Y)] })#lambda]( trace, diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index ca09d2977..7bed24320 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -39,8 +39,6 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { override def isNothing(b: B): Boolean = self.isNothing(f(b)) - override def isEmpty(b: B): Boolean = self.isEmpty(f(b)) - override final def toJsonAST(b: B): Either[String, Json] = self.toJsonAST(f(b)) } @@ -80,11 +78,6 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { */ def isNothing(a: A): Boolean = false - /** - * This default may be overridden when this value may be empty within a JSON object and still be encoded. - */ - def isEmpty(a: A): Boolean = false - /** * Returns this encoder but narrowed to the its given sub-type */ @@ -209,8 +202,6 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def isNothing(a: A): Boolean = encoder.isNothing(a) - override def isEmpty(a: A): Boolean = encoder.isEmpty(a) - override def toJsonAST(a: A): Either[String, Json] = encoder.toJsonAST(a) } @@ -320,14 +311,8 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { this: JsonEncoder.type => - implicit def array[A](implicit - A: JsonEncoder[A], - classTag: ClassTag[A] - ): JsonEncoder[Array[A]] = + implicit def array[A](implicit A: JsonEncoder[A], classTag: ClassTag[A]): JsonEncoder[Array[A]] = new JsonEncoder[Array[A]] { - - override def isEmpty(as: Array[A]): Boolean = as.isEmpty - def unsafeEncode(as: Array[A], indent: Option[Int], out: Write): Unit = if (as.isEmpty) out.write("[]") else { @@ -389,11 +374,9 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { implicit def vector[A: JsonEncoder]: JsonEncoder[Vector[A]] = iterable[A, Vector] - implicit def set[A: JsonEncoder]: JsonEncoder[Set[A]] = - iterable[A, Set] + implicit def set[A: JsonEncoder]: JsonEncoder[Set[A]] = iterable[A, Set] - implicit def hashSet[A: JsonEncoder]: JsonEncoder[immutable.HashSet[A]] = - iterable[A, immutable.HashSet] + implicit def hashSet[A: JsonEncoder]: JsonEncoder[immutable.HashSet[A]] = iterable[A, immutable.HashSet] implicit def sortedSet[A: Ordering: JsonEncoder]: JsonEncoder[immutable.SortedSet[A]] = iterable[A, immutable.SortedSet] @@ -417,13 +400,8 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { this: JsonEncoder.type => - implicit def iterable[A, T[X] <: Iterable[X]](implicit - A: JsonEncoder[A] - ): JsonEncoder[T[A]] = + implicit def iterable[A, T[X] <: Iterable[X]](implicit A: JsonEncoder[A]): JsonEncoder[T[A]] = new JsonEncoder[T[A]] { - - override def isEmpty(as: T[A]): Boolean = as.isEmpty - def unsafeEncode(as: T[A], indent: Option[Int], out: Write): Unit = if (as.isEmpty) out.write("[]") else { @@ -471,9 +449,6 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { K: JsonFieldEncoder[K], A: JsonEncoder[A] ): JsonEncoder[T[K, A]] = new JsonEncoder[T[K, A]] { - - override def isEmpty(a: T[K, A]): Boolean = a.isEmpty - def unsafeEncode(kvs: T[K, A], indent: Option[Int], out: Write): Unit = if (kvs.isEmpty) out.write("{}") else { diff --git a/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala index 429c93c1c..e6769d013 100644 --- a/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala @@ -2,10 +2,6 @@ package zio.json import zio.json.ast.Json import zio.test._ -import zio.Chunk - -import scala.collection.immutable -import scala.collection.mutable object AnnotationsCodecSpec extends ZIOSpecDefault { @@ -90,17 +86,6 @@ object AnnotationsCodecSpec extends ZIOSpecDefault { expectedStr.fromJson[OptionalField].toOption.get == expectedObj, expectedObj.toJson == expectedStr ) - }, - test("do not write empty collections") { - @jsonExplicitEmptyCollection(false) - case class EmptySeq(a: Seq[Int]) - - val expectedStr = """{}""" - val expectedObj = EmptySeq(Seq.empty) - - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) } ), suite("AST")( @@ -159,398 +144,6 @@ object AnnotationsCodecSpec extends ZIOSpecDefault { implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen assertTrue(jsonAST.as[OptionalField].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) - }, - test("do not write empty collections") { - @jsonExplicitEmptyCollection(false) - case class EmptySeq(a: Seq[Int]) - - val jsonAST = Json.Obj("a" -> Json.Arr()) - val expectedObj = EmptySeq(Seq.empty) - - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - - assertTrue(jsonAST.as[EmptySeq].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) - } - ) - ), - suite("explicit empty collections")( - suite("should write empty collections if set to true")( - test("for an array") { - @jsonExplicitEmptyCollection(true) - case class EmptyArray(a: Array[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyArray(Array.empty) - - implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyArray].toOption.get.a.isEmpty, expectedObj.toJson == expectedStr) - }, - test("for a seq") { - @jsonExplicitEmptyCollection(true) - case class EmptySeq(a: Seq[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptySeq(Seq.empty) - - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a chunk") { - @jsonExplicitEmptyCollection(true) - case class EmptyChunk(a: Chunk[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyChunk(Chunk.empty) - - implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyChunk].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for an indexed seq") { - case class EmptyIndexedSeq(a: IndexedSeq[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) - - implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyIndexedSeq].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a linear seq") { - @jsonExplicitEmptyCollection(true) - case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) - - implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyLinearSeq].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a list set") { - @jsonExplicitEmptyCollection(true) - case class EmptyListSet(a: immutable.ListSet[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyListSet(immutable.ListSet.empty) - - implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyListSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a tree set") { - @jsonExplicitEmptyCollection(true) - case class EmptyTreeSet(a: immutable.TreeSet[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) - - implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyTreeSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a list") { - @jsonExplicitEmptyCollection(true) - case class EmptyList(a: List[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyList(List.empty) - - implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyList].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a vector") { - @jsonExplicitEmptyCollection(true) - case class EmptyVector(a: Vector[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyVector(Vector.empty) - - implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyVector].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a set") { - @jsonExplicitEmptyCollection(true) - case class EmptySet(a: Set[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptySet(Set.empty) - - implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a hash set") { - @jsonExplicitEmptyCollection(true) - case class EmptyHashSet(a: immutable.HashSet[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyHashSet(immutable.HashSet.empty) - - implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyHashSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a sorted set") { - @jsonExplicitEmptyCollection(true) - case class EmptySortedSet(a: immutable.SortedSet[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptySortedSet(immutable.SortedSet.empty) - - implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptySortedSet].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a map") { - @jsonExplicitEmptyCollection(true) - case class EmptyMap(a: Map[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptyMap(Map.empty) - - implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a hash map") { - @jsonExplicitEmptyCollection(true) - case class EmptyHashMap(a: immutable.HashMap[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptyHashMap(immutable.HashMap.empty) - - implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyHashMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a mutable map") { - @jsonExplicitEmptyCollection(true) - case class EmptyMutableMap(a: mutable.Map[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptyMutableMap(mutable.Map.empty) - - implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyMutableMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a sorted map") { - @jsonExplicitEmptyCollection(true) - case class EmptySortedMap(a: collection.SortedMap[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptySortedMap(collection.SortedMap.empty) - - implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptySortedMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a list map") { - @jsonExplicitEmptyCollection(true) - case class EmptyListMap(a: immutable.ListMap[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptyListMap(immutable.ListMap.empty) - - implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyListMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - } - ), - suite("should not write empty collections if set to false")( - test("for an array") { - @jsonExplicitEmptyCollection(false) - case class EmptyArray(a: Array[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyArray(Array.empty) - - implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyArray].toOption.get.a.isEmpty, expectedObj.toJson == expectedStr) - }, - test("for a seq") { - @jsonExplicitEmptyCollection(false) - case class EmptySeq(a: Seq[Int]) - val expectedStr = """{}""" - val expectedObj = EmptySeq(Seq.empty) - - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a chunk") { - @jsonExplicitEmptyCollection(false) - case class EmptyChunk(a: Chunk[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyChunk(Chunk.empty) - - implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyChunk].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for an indexed seq") { - @jsonExplicitEmptyCollection(false) - case class EmptyIndexedSeq(a: IndexedSeq[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) - - implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyIndexedSeq].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a linear seq") { - @jsonExplicitEmptyCollection(false) - case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) - - implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyLinearSeq].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a list set") { - @jsonExplicitEmptyCollection(false) - case class EmptyListSet(a: immutable.ListSet[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyListSet(immutable.ListSet.empty) - - implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyListSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a treeSet") { - @jsonExplicitEmptyCollection(false) - case class EmptyTreeSet(a: immutable.TreeSet[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) - - implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyTreeSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a list") { - @jsonExplicitEmptyCollection(false) - case class EmptyList(a: List[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyList(List.empty) - - implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyList].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a vector") { - @jsonExplicitEmptyCollection(false) - case class EmptyVector(a: Vector[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyVector(Vector.empty) - - implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyVector].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a set") { - @jsonExplicitEmptyCollection(false) - case class EmptySet(a: Set[Int]) - val expectedStr = """{}""" - val expectedObj = EmptySet(Set.empty) - - implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a hash set") { - @jsonExplicitEmptyCollection(false) - case class EmptyHashSet(a: immutable.HashSet[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyHashSet(immutable.HashSet.empty) - - implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyHashSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a sorted set") { - @jsonExplicitEmptyCollection(false) - case class EmptySortedSet(a: immutable.SortedSet[Int]) - val expectedStr = """{}""" - val expectedObj = EmptySortedSet(immutable.SortedSet.empty) - - implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptySortedSet].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a map") { - @jsonExplicitEmptyCollection(false) - case class EmptyMap(a: Map[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptyMap(Map.empty) - - implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a hashMap") { - @jsonExplicitEmptyCollection(false) - case class EmptyHashMap(a: immutable.HashMap[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptyHashMap(immutable.HashMap.empty) - - implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyHashMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a mutable map") { - @jsonExplicitEmptyCollection(false) - case class EmptyMutableMap(a: mutable.Map[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptyMutableMap(mutable.Map.empty) - - implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyMutableMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a sorted map") { - @jsonExplicitEmptyCollection(false) - case class EmptySortedMap(a: collection.SortedMap[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptySortedMap(collection.SortedMap.empty) - - implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptySortedMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a list map") { - @jsonExplicitEmptyCollection(false) - case class EmptyListMap(a: immutable.ListMap[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptyListMap(immutable.ListMap.empty) - - implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyListMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) } ) ) diff --git a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala index 75671f80a..8fc2df6cf 100644 --- a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala @@ -3,10 +3,6 @@ package zio.json import zio.json.JsonCodecConfiguration.SumTypeHandling.DiscriminatorField import zio.json.ast.Json import zio.test._ -import zio.Chunk - -import scala.collection.immutable -import scala.collection.mutable object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { case class ClassWithFields(someField: Int, someOtherField: String) @@ -54,29 +50,43 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { assertTrue( jsonStr.fromJson[ClassWithFields].toOption.get == expectedObj ) - } - ), - test("do not write nulls by default") { - val expectedStr = """{}""" - val expectedObj = OptionalField(None) + }, + test("do not write nulls by default, decode missing nulls as None") { + val expectedStr = """{}""" + val expectedObj = OptionalField(None) - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen - assertTrue( - expectedStr.fromJson[OptionalField].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("write empty collections by default") { - case class EmptySeq(a: Seq[Int]) + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("write empty collections by default") { + case class EmptySeq(a: Seq[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptySeq(Seq.empty) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySeq(Seq.empty) - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, + assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("fail on decoding missing empty collections by default") { + case class Empty(z: Option[Int]) + case class EmptyObj(a: Empty) + case class EmptySeq(a: Seq[Int]) + + implicit val codecEmpty: JsonCodec[Empty] = DeriveJsonCodec.gen[Empty] + implicit val codecEmptyObj: JsonCodec[EmptyObj] = DeriveJsonCodec.gen[EmptyObj] + implicit val codecEmptySeq: JsonCodec[EmptySeq] = DeriveJsonCodec.gen[EmptySeq] + + assertTrue( + """{}""".fromJson[EmptyObj] == Left(".a(missing)"), + """{}""".fromJson[EmptySeq] == Left(".a(missing)") + ) + } + ), suite("AST")( test("should not map field names by default") { val expectedAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a")) @@ -122,12 +132,14 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { ) }, test("write empty collections by default") { - case class EmptySeq(a: Seq[Int]) + case class Empty() + case class EmptySeq(a: Seq[Int], b: Empty) - val jsonAST = Json.Obj("a" -> Json.Arr()) - val expectedObj = EmptySeq(Seq.empty) + val jsonAST = Json.Obj("a" -> Json.Arr(), "b" -> Json.Obj()) + val expectedObj = EmptySeq(Seq.empty, Empty()) - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + implicit val emptyCodec: JsonCodec[Empty] = DeriveJsonCodec.gen + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen assertTrue( jsonAST.as[EmptySeq].toOption.get == expectedObj, @@ -200,18 +212,6 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { expectedStr.fromJson[OptionalField].toOption.get == expectedObj, expectedObj.toJson == expectedStr ) - }, - test("do not write empty collections") { - case class EmptySeq(a: Seq[Int]) - - val expectedStr = """{}""" - val expectedObj = EmptySeq(Seq.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) } ), suite("AST")( @@ -262,434 +262,15 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { assertTrue(jsonAST.as[OptionalField].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) }, - test("do not write empty collections") { - case class EmptySeq(a: Seq[Int]) - - val jsonAST = Json.Obj("a" -> Json.Arr()) - val expectedObj = EmptySeq(Seq.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - - assertTrue(jsonAST.as[EmptySeq].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) - } - ) - ), - suite("explicit empty collections")( - suite("should write empty collections if set to true")( - test("for an array") { - case class EmptyArray(a: Array[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyArray(Array.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyArray].toOption.get.a.isEmpty, expectedObj.toJson == expectedStr) - }, - test("for a seq") { - case class EmptySeq(a: Seq[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptySeq(Seq.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a chunk") { - case class EmptyChunk(a: Chunk[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyChunk(Chunk.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyChunk].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for an indexed seq") { - case class EmptyIndexedSeq(a: IndexedSeq[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyIndexedSeq].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a linear seq") { - case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyLinearSeq].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a list set") { - case class EmptyListSet(a: immutable.ListSet[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyListSet(immutable.ListSet.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyListSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a tree set") { - case class EmptyTreeSet(a: immutable.TreeSet[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyTreeSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a list") { - case class EmptyList(a: List[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyList(List.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyList].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a vector") { - case class EmptyVector(a: Vector[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyVector(Vector.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyVector].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a set") { - case class EmptySet(a: Set[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptySet(Set.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a hash set") { - case class EmptyHashSet(a: immutable.HashSet[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptyHashSet(immutable.HashSet.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyHashSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a sorted set") { - case class EmptySortedSet(a: immutable.SortedSet[Int]) - val expectedStr = """{"a":[]}""" - val expectedObj = EmptySortedSet(immutable.SortedSet.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptySortedSet].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a map") { - case class EmptyMap(a: Map[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptyMap(Map.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a hash map") { - case class EmptyHashMap(a: immutable.HashMap[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptyHashMap(immutable.HashMap.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyHashMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a mutable map") { - case class EmptyMutableMap(a: mutable.Map[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptyMutableMap(mutable.Map.empty) + test("fail on decoding missing explicit nulls") { + val jsonStr = """{}""" implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyMutableMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a sorted map") { - case class EmptySortedMap(a: collection.SortedMap[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptySortedMap(collection.SortedMap.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptySortedMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a list map") { - case class EmptyListMap(a: immutable.ListMap[String, String]) - val expectedStr = """{"a":{}}""" - val expectedObj = EmptyListMap(immutable.ListMap.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = true) - implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyListMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - } - ), - suite("should not write empty collections if set to false")( - test("for an array") { - case class EmptyArray(a: Array[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyArray(Array.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyArray].toOption.get.a.isEmpty, expectedObj.toJson == expectedStr) - }, - test("for a seq") { - case class EmptySeq(a: Seq[Int]) - val expectedStr = """{}""" - val expectedObj = EmptySeq(Seq.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a chunk") { - case class EmptyChunk(a: Chunk[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyChunk(Chunk.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyChunk].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for an indexed seq") { - case class EmptyIndexedSeq(a: IndexedSeq[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyIndexedSeq].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a linear seq") { - case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyLinearSeq].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a list set") { - case class EmptyListSet(a: immutable.ListSet[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyListSet(immutable.ListSet.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyListSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a treeSet") { - case class EmptyTreeSet(a: immutable.TreeSet[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyTreeSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a list") { - case class EmptyList(a: List[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyList(List.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyList].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a vector") { - case class EmptyVector(a: Vector[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyVector(Vector.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyVector].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a set") { - case class EmptySet(a: Set[Int]) - val expectedStr = """{}""" - val expectedObj = EmptySet(Set.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptySet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a hash set") { - case class EmptyHashSet(a: immutable.HashSet[Int]) - val expectedStr = """{}""" - val expectedObj = EmptyHashSet(immutable.HashSet.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyHashSet].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a sorted set") { - case class EmptySortedSet(a: immutable.SortedSet[Int]) - val expectedStr = """{}""" - val expectedObj = EmptySortedSet(immutable.SortedSet.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptySortedSet].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a map") { - case class EmptyMap(a: Map[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptyMap(Map.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a hashMap") { - case class EmptyHashMap(a: immutable.HashMap[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptyHashMap(immutable.HashMap.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen - - assertTrue(expectedStr.fromJson[EmptyHashMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - }, - test("for a mutable map") { - case class EmptyMutableMap(a: mutable.Map[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptyMutableMap(mutable.Map.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptyMutableMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a sorted map") { - case class EmptySortedMap(a: collection.SortedMap[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptySortedMap(collection.SortedMap.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen - - assertTrue( - expectedStr.fromJson[EmptySortedMap].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - }, - test("for a list map") { - case class EmptyListMap(a: immutable.ListMap[String, String]) - val expectedStr = """{}""" - val expectedObj = EmptyListMap(immutable.ListMap.empty) - - implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitEmptyCollections = false) - implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitNulls = true) + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen - assertTrue(expectedStr.fromJson[EmptyListMap].toOption.get == expectedObj, expectedObj.toJson == expectedStr) - } + assertTrue(jsonStr.fromJson[OptionalField].isLeft) + } @@ TestAspect.ignore ) ) ) From 49483e129352eddb42410067435efd2e74f9849a Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:40:19 +0100 Subject: [PATCH 082/311] fix: always evaluate default values (in case it is a method) (#1237) --- docs/decoding.md | 2 +- .../src/main/scala-2.x/zio/json/macros.scala | 16 ++++---- .../src/main/scala-3/zio/json/macros.scala | 10 ++--- .../src/test/scala/zio/json/DecoderSpec.scala | 38 +++++++++++++++++++ 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/docs/decoding.md b/docs/decoding.md index cb8d7abb1..596aab4db 100644 --- a/docs/decoding.md +++ b/docs/decoding.md @@ -36,7 +36,7 @@ Now we can parse JSON into our object ### Automatic Derivation and case class default field values -If a case class field is defined with a default value and the field is not present or `null`, the default value will be used. +If a case class field is defined with a default value and the field is not present or `null`, the default value will be used (or evaluated when it is a method). Say we have a Scala `case class` diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 7ec5366bd..7411de817 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -259,10 +259,10 @@ object DeriveJsonDecoder { } (names, aliases) } - private[this] val len = names.length - private[this] val matrix = new StringMatrix(names, aliases) - private[this] val spans = names.map(JsonError.ObjectAccess) - private[this] val defaults = ctx.parameters.map(_.default).toArray + private[this] val len = names.length + private[this] val matrix = new StringMatrix(names, aliases) + private[this] val spans = names.map(JsonError.ObjectAccess) + private[this] lazy val defaults = ctx.parameters.map(_.evaluateDefault).toArray private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap @@ -291,7 +291,7 @@ object DeriveJsonDecoder { true } ) tcs(idx).unsafeDecode(spans(idx) :: trace, in) - else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get() else Lexer.error("expected 'null'", spans(idx) :: trace) } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) @@ -301,7 +301,7 @@ object DeriveJsonDecoder { if (ps(idx) == null) { val default = defaults(idx) ps(idx) = - if (default ne None) default.get + if (default ne None) default.get() else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } idx += 1 @@ -320,7 +320,7 @@ object DeriveJsonDecoder { if (ps(idx) != null) Lexer.error("duplicate", trace) val default = defaults(idx) ps(idx) = - if ((default ne None) && (value eq Json.Null)) default.get + if ((default ne None) && (value eq Json.Null)) default.get() else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, value) case _ => if (no_extra) Lexer.error("invalid extra field", trace) @@ -331,7 +331,7 @@ object DeriveJsonDecoder { if (ps(idx) == null) { val default = defaults(idx) ps(idx) = - if (default ne None) default.get + if (default ne None) default.get() else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } idx += 1 diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index e7f8ae583..4beeafeb8 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -278,7 +278,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv private val len = names.length private val matrix = new StringMatrix(names, aliases) private val spans = names.map(JsonError.ObjectAccess(_)) - private val defaults = IArray.genericWrapArray(ctx.params.map(_.default)).toArray + private lazy val defaults = IArray.genericWrapArray(ctx.params.map(_.evaluateDefault)).toArray private lazy val tcs = IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap @@ -296,7 +296,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv in.retract() true }) tcs(idx).unsafeDecode(spans(idx) :: trace, in) - else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get() else Lexer.error("expected 'null'", spans(idx) :: trace) } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) @@ -307,7 +307,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (ps(idx) == null) { val default = defaults(idx) ps(idx) = - if (default ne None) default.get + if (default ne None) default.get() else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } idx += 1 @@ -325,7 +325,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (ps(idx) != null) Lexer.error("duplicate", trace) val default = defaults(idx) ps(idx) = - if ((default ne None) && (value eq Json.Null)) default.get + if ((default ne None) && (value eq Json.Null)) default.get() else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, value) case _ => if (no_extra) Lexer.error("invalid extra field", trace) @@ -336,7 +336,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (ps(idx) == null) { val default = defaults(idx) ps(idx) = - if (default ne None) default.get + if (default ne None) default.get() else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } idx += 1 diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 3d1ab308f..6c15d929c 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -157,6 +157,25 @@ object DecoderSpec extends ZIOSpecDefault { assert("""{}""".fromJson[DefaultString])(isRight(equalTo(DefaultString("")))) && assert("""{"s": null}""".fromJson[DefaultString])(isRight(equalTo(DefaultString("")))) }, + test("dynamic default value") { + case class DefaultDynamic( + randomNumber: Double = scala.math.random(), + instant: java.time.Instant = java.time.Instant.now() + ) + + object DefaultDynamic { + implicit lazy val decoder: JsonDecoder[DefaultDynamic] = DeriveJsonDecoder.gen[DefaultDynamic] + } + + def res = """{}""".stripMargin.fromJson[DefaultDynamic] + + for { + dynamics1 <- ZIO.fromEither(res) + _ <- ZIO.sleep(2.millis) + dynamics2 <- ZIO.fromEither(res) + } yield assertTrue(dynamics1.randomNumber != dynamics2.randomNumber) && + assertTrue(dynamics1.instant != dynamics2.instant) + } @@ TestAspect.withLiveClock, test("sum encoding") { import examplesum._ @@ -441,6 +460,25 @@ object DecoderSpec extends ZIOSpecDefault { assert(Json.Obj().as[DefaultString])(isRight(equalTo(DefaultString("")))) && assert(Json.Obj("s" -> Json.Null).as[DefaultString])(isRight(equalTo(DefaultString("")))) }, + test("dynamic default value") { + case class DefaultDynamic( + randomNumber: Double = scala.math.random(), + instant: java.time.Instant = java.time.Instant.now() + ) + + object DefaultDynamic { + implicit lazy val decoder: JsonDecoder[DefaultDynamic] = DeriveJsonDecoder.gen[DefaultDynamic] + } + + for { + dynamics1 <- ZIO.fromEither(Json.Obj().as[DefaultDynamic]) + _ <- ZIO.sleep(2.millis) // ensure java.time.Instant is different + dynamics2 <- ZIO.fromEither(Json.Obj().as[DefaultDynamic]) + } yield assertTrue( + dynamics1.randomNumber != dynamics2.randomNumber, + dynamics1.instant != dynamics2.instant + ) + } @@ TestAspect.withLiveClock, test("aliases") { import exampleproducts._ From 8d140c3093451b92b13eea93a8af68550c33d0a6 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 23 Jan 2025 15:38:49 +0100 Subject: [PATCH 083/311] Add missing base branch when creating PR for README update --- .github/workflows/site.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 6e1f40ea9..36f866b3f 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -89,4 +89,5 @@ jobs: branch: zio-sbt-website/update-readme commit-message: Update README.md delete-branch: true + base: series/2.x title: Update README.md From f3f8c9fc7a8852823fb89d6af3cd2cc6da68c5ee Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 23 Jan 2025 20:36:29 +0100 Subject: [PATCH 084/311] Remove redundant `lazy` in case class decoders (#1238) --- zio-json/shared/src/main/scala-2.x/zio/json/macros.scala | 8 ++++---- zio-json/shared/src/main/scala-3/zio/json/macros.scala | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 7411de817..cd1b9ccfe 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -259,10 +259,10 @@ object DeriveJsonDecoder { } (names, aliases) } - private[this] val len = names.length - private[this] val matrix = new StringMatrix(names, aliases) - private[this] val spans = names.map(JsonError.ObjectAccess) - private[this] lazy val defaults = ctx.parameters.map(_.evaluateDefault).toArray + private[this] val len = names.length + private[this] val matrix = new StringMatrix(names, aliases) + private[this] val spans = names.map(JsonError.ObjectAccess) + private[this] val defaults = ctx.parameters.map(_.evaluateDefault).toArray private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 4beeafeb8..209ee7154 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -278,7 +278,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv private val len = names.length private val matrix = new StringMatrix(names, aliases) private val spans = names.map(JsonError.ObjectAccess(_)) - private lazy val defaults = IArray.genericWrapArray(ctx.params.map(_.evaluateDefault)).toArray + private val defaults = IArray.genericWrapArray(ctx.params.map(_.evaluateDefault)).toArray private lazy val tcs = IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap From dfb8184331ec9369c4df8886d145acbd4543257b Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 24 Jan 2025 08:24:24 +0100 Subject: [PATCH 085/311] Fix skipping of escaped JSON string (#1240) --- .../main/scala/zio/json/internal/lexer.scala | 5 +++-- .../scala-3/zio/json/DerivedDecoderSpec.scala | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 12e03d9f7..0d9fb2587 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -151,11 +151,12 @@ object Lexer { } @tailrec - private def skipString(in: OneCharReader, evenBackSlashes: Boolean): Unit = + private def skipString(in: OneCharReader, evenBackSlashes: Boolean): Unit = { + val ch = in.readChar() if (evenBackSlashes) { - val ch = in.readChar() if (ch != '"') skipString(in, ch != '\\') } else skipString(in, evenBackSlashes = true) + } @tailrec private def skipObject(in: OneCharReader, level: Int): Unit = { diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala index e117e1ebe..96001af75 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala @@ -45,6 +45,28 @@ object DerivedDecoderSpec extends ZIOSpecDefault { assertTrue("""{"$type":"Qux"}""".fromJson[Foo] == Right(Foo.Qux)) assertTrue("""{"$type":"Barrr"}""".fromJson[Foo] == Right(Foo.Bar)) }, + test("skip JSON encoded in a string value") { + @jsonDiscriminator("type") + sealed trait Example derives JsonDecoder { + type Content + def content: Content + } + object Example { + @jsonHint("JSON") + final case class JsonInput(content: String) extends Example { + override type Content = String + } + } + + val json = + """ + |{ + | "content": "\"{\\n \\\"name\\\": \\\"John\\\",\\\"location\\\":\\\"Sydney\\\",\\n \\\"email\\\": \\\"jdoe@test.com\\\"\\n}\"", + | "type": "JSON" + |} + |""".stripMargin.trim + assertTrue(json.fromJson[Example].isRight) + }, test("Derives for a recursive sum ADT type") { enum Foo derives JsonDecoder: case Bar From 8b800e0ba10b33be29723e674b6ee934bca3982d Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 24 Jan 2025 08:43:55 +0100 Subject: [PATCH 086/311] Add `Json.Obj.empty` and `Json.Arr.empty` constants (#1241) --- zio-json/shared/src/main/scala/zio/json/ast/ast.scala | 4 ++++ zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index 3fac2459e..95e94d372 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -378,6 +378,8 @@ object Json { override def mapObjectEntries(f: ((String, Json)) => (String, Json)): Json.Obj = Json.Obj(fields.map(f)) } object Obj { + val empty: Obj = Obj(Chunk.empty) + def apply(fields: (String, Json)*): Obj = Obj(Chunk(fields: _*)) private lazy val objd = JsonDecoder.keyValueChunk[String, Json] @@ -423,6 +425,8 @@ object Json { override def mapArrayValues(f: Json => Json): Json.Arr = Json.Arr(elements.map(f)) } object Arr { + val empty: Arr = Arr(Chunk.empty) + def apply(elements: Json*): Arr = Arr(Chunk(elements: _*)) private lazy val arrd = JsonDecoder.chunk[Json] diff --git a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala index 069f16594..dca36e625 100644 --- a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala @@ -1,5 +1,6 @@ package zio.json.ast +import zio.Chunk import zio.json._ import zio.test.Assertion._ import zio.test._ @@ -87,6 +88,8 @@ object JsonSpec extends ZIOSpecDefault { case fst :: snd :: Nil => fst != snd case _ => false }) + assertTrue(Json.Obj.empty == Json.Obj(Chunk.empty)) + assertTrue(Json.Arr.empty == Json.Arr(Chunk.empty)) }, test("object order does not matter for equality") { val obj1 = Json.Obj( @@ -146,6 +149,7 @@ object JsonSpec extends ZIOSpecDefault { ) assertTrue(obj1.hashCode == obj2.hashCode) + assertTrue(Json.Obj.empty.hashCode == Json.Obj(Chunk.empty).hashCode) } ), suite("foldUp")( From 4921573e2180f507c221f760a7ef3b5572b60192 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:07:41 +0100 Subject: [PATCH 087/311] Update magnolia to 1.3.9 (#1226) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 4f17c7b7a..6e6fdd97f 100644 --- a/build.sbt +++ b/build.sbt @@ -122,7 +122,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Seq( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.7" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.9" ) case _ => Seq( From 052ac66dab33a73a1217b56ea77b276dc0d08a31 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:10:37 +0100 Subject: [PATCH 088/311] Update sbt-ci-release to 1.9.2 (#1198) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 2d491132c..9537df73a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.6.1") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.2") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") From aeb0964b91d85040435edf3410a6e40e1bb415d9 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:14:20 +0100 Subject: [PATCH 089/311] Update sbt, scripted-plugin to 1.10.7 (#1200) --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index db1723b08..73df629ac 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.5 +sbt.version=1.10.7 From b7e453bf33b491c70503dd12be98571a9c43f489 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:16:19 +0100 Subject: [PATCH 090/311] Update scala-library, scala-reflect to 2.12.20 (#1157) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7b8f34ee..d938feef7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: fail-fast: false matrix: java: ['17', '21'] - scala: ['2.12.19', '2.13.15', '3.3.4'] + scala: ['2.12.20', '2.13.15', '3.3.4'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch From 0860538d080d19cfe32b8370683e8e3856b3846e Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 24 Jan 2025 14:58:27 +0100 Subject: [PATCH 091/311] Revert "Update scala-library, scala-reflect to 2.12.20 (#1157)" (#1242) This reverts commit b7e453bf33b491c70503dd12be98571a9c43f489. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d938feef7..b7b8f34ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: fail-fast: false matrix: java: ['17', '21'] - scala: ['2.12.20', '2.13.15', '3.3.4'] + scala: ['2.12.19', '2.13.15', '3.3.4'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch From 4f1bddab71c7f439ae25813ee6348232285b17ed Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 25 Jan 2025 08:57:17 +0100 Subject: [PATCH 092/311] More efficient `JsonFieldEncoder` implementations for `Int`, `Long`, and `UUID` types (#1244) --- .../main/scala/zio/json/JsonFieldEncoder.scala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala index d2b1e156c..a5c19eefb 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala @@ -33,13 +33,15 @@ object JsonFieldEncoder { def unsafeEncodeField(in: String): String = in } - implicit val int: JsonFieldEncoder[Int] = - JsonFieldEncoder[String].contramap(_.toString) - - implicit val long: JsonFieldEncoder[Long] = - JsonFieldEncoder[String].contramap(_.toString) + implicit val int: JsonFieldEncoder[Int] = new JsonFieldEncoder[Int] { + def unsafeEncodeField(in: Int): String = in.toString + } - implicit val uuid: JsonFieldEncoder[java.util.UUID] = - JsonFieldEncoder[String].contramap(_.toString) + implicit val long: JsonFieldEncoder[Long] = new JsonFieldEncoder[Long] { + def unsafeEncodeField(in: Long): String = in.toString + } + implicit val uuid: JsonFieldEncoder[java.util.UUID] = new JsonFieldEncoder[java.util.UUID] { + def unsafeEncodeField(in: java.util.UUID): String = in.toString + } } From 794aad8e1a1ca45b20094d265d3a484c63b2263c Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 25 Jan 2025 09:22:27 +0100 Subject: [PATCH 093/311] Turn off stack traces for `RewindTwice` exception (#1245) --- zio-json/shared/src/main/scala/zio/json/internal/readers.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala index eece2aa09..e454e0212 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala @@ -71,6 +71,7 @@ private[zio] final class RewindTwice extends Exception( "RecordingReader's rewind was called twice" ) + with NoStackTrace /** * A Reader that can retract and replay the last char that it read. From b1aaf9c4922aed0838308da3931528fbbc11f759 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 25 Jan 2025 11:43:45 +0100 Subject: [PATCH 094/311] Fix decoding of "Infinity", "-Infinity", and "NaN" for `Double` and `Float` values (#1246) --- README.md | 2 +- docs/index.md | 2 +- .../json/DecoderPlatformSpecificSpec.scala | 4 +- .../main/scala/zio/json/internal/lexer.scala | 33 +++++------ .../src/test/scala/zio/json/DecoderSpec.scala | 58 +++++++++++++++++-- 5 files changed, 73 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 171044685..800551f60 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ And bad JSON will produce an error in `jq` syntax with an additional piece of co ``` scala> """{"curvature": womp}""".fromJson[Banana] -val res: Either[String, Banana] = Left(.curvature(expected a number, got w)) +val res: Either[String, Banana] = Left(.curvature(expected a Double)) ``` Say we extend our data model to include more data types diff --git a/docs/index.md b/docs/index.md index 205c70050..9707491aa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -89,7 +89,7 @@ And bad JSON will produce an error in `jq` syntax with an additional piece of co ``` scala> """{"curvature": womp}""".fromJson[Banana] -val res: Either[String, Banana] = Left(.curvature(expected a number, got w)) +val res: Either[String, Banana] = Left(.curvature(expected a Double)) ``` Say we extend our data model to include more data types diff --git a/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala b/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala index 10cfacfae..9e07b5517 100644 --- a/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala +++ b/zio-json/jvm/src/test/scala/zio/json/DecoderPlatformSpecificSpec.scala @@ -262,11 +262,11 @@ object DecoderPlatformSpecificSpec extends ZIOSpecDefault { test("test hand-coded alternative in `orElse` comment") { val decoder: JsonDecoder[AnyVal] = JsonDecoder.peekChar[AnyVal] { case 't' | 'f' => JsonDecoder[Boolean].widen - case c => JsonDecoder[Int].widen + case _ => JsonDecoder[Int].widen } assert(decoder.decodeJson("true"))(equalTo(Right(true.asInstanceOf[AnyVal]))) && assert(decoder.decodeJson("42"))(equalTo(Right(42.asInstanceOf[AnyVal]))) && - assert(decoder.decodeJson("\"a string\""))(equalTo(Left("(expected a number, got 'a')"))) + assert(decoder.decodeJson("\"a string\""))(equalTo(Left("(expected an Int)"))) } ) ) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 0d9fb2587..ba7b7b5c9 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -286,7 +286,8 @@ object Lexer { } def byte(trace: List[JsonError], in: RetractReader): Byte = { - checkNumber(trace, in) + in.nextNonWhitespace() + in.retract() try { val i = UnsafeNumbers.byte_(in, false) in.retract() @@ -297,7 +298,8 @@ object Lexer { } def short(trace: List[JsonError], in: RetractReader): Short = { - checkNumber(trace, in) + in.nextNonWhitespace() + in.retract() try { val i = UnsafeNumbers.short_(in, false) in.retract() @@ -308,7 +310,8 @@ object Lexer { } def int(trace: List[JsonError], in: RetractReader): Int = { - checkNumber(trace, in) + in.nextNonWhitespace() + in.retract() try { val i = UnsafeNumbers.int_(in, false) in.retract() @@ -319,7 +322,8 @@ object Lexer { } def long(trace: List[JsonError], in: RetractReader): Long = { - checkNumber(trace, in) + in.nextNonWhitespace() + in.retract() try { val i = UnsafeNumbers.long_(in, false) in.retract() @@ -333,7 +337,8 @@ object Lexer { trace: List[JsonError], in: RetractReader ): java.math.BigInteger = { - checkNumber(trace, in) + in.nextNonWhitespace() + in.retract() try { val i = UnsafeNumbers.bigInteger_(in, false, NumberMaxBits) in.retract() @@ -344,7 +349,8 @@ object Lexer { } def float(trace: List[JsonError], in: RetractReader): Float = { - checkNumber(trace, in) + in.nextNonWhitespace() + in.retract() try { val i = UnsafeNumbers.float_(in, false, NumberMaxBits) in.retract() @@ -355,7 +361,8 @@ object Lexer { } def double(trace: List[JsonError], in: RetractReader): Double = { - checkNumber(trace, in) + in.nextNonWhitespace() + in.retract() try { val i = UnsafeNumbers.double_(in, false, NumberMaxBits) in.retract() @@ -369,7 +376,8 @@ object Lexer { trace: List[JsonError], in: RetractReader ): java.math.BigDecimal = { - checkNumber(trace, in) + in.nextNonWhitespace() + in.retract() try { val i = UnsafeNumbers.bigDecimal_(in, false, NumberMaxBits) in.retract() @@ -379,15 +387,6 @@ object Lexer { } } - // really just a way to consume the whitespace - private def checkNumber(trace: List[JsonError], in: RetractReader): Unit = { - (in.nextNonWhitespace(): @switch) match { - case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => () - case c => error("a number,", c, trace) - } - in.retract() - } - // optional whitespace and then an expected character @inline def char(trace: List[JsonError], in: OneCharReader, c: Char): Unit = { val got = in.nextNonWhitespace() diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 6c15d929c..aac714d8e 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -16,13 +16,61 @@ object DecoderSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = suite("Decoder")( suite("fromJson")( + test("byte") { + assert("-123".fromJson[Byte])(isRight(equalTo(-123: Byte))) && + assert("\"-123\"".fromJson[Byte])(isRight(equalTo(-123: Byte))) && + assertTrue("\"Infinity\"".fromJson[Byte].isLeft) && + assertTrue("\"-Infinity\"".fromJson[Byte].isLeft) && + assertTrue("\"NaN\"".fromJson[Byte].isLeft) + }, + test("short") { + assert("-12345".fromJson[Short])(isRight(equalTo(-12345: Short))) && + assert("\"-12345\"".fromJson[Short])(isRight(equalTo(-12345: Short))) && + assertTrue("\"Infinity\"".fromJson[Short].isLeft) && + assertTrue("\"-Infinity\"".fromJson[Short].isLeft) && + assertTrue("\"NaN\"".fromJson[Short].isLeft) + }, + test("int") { + assert("-1234567890".fromJson[Int])(isRight(equalTo(-1234567890))) && + assert("\"-1234567890\"".fromJson[Int])(isRight(equalTo(-1234567890))) && + assertTrue("\"Infinity\"".fromJson[Int].isLeft) && + assertTrue("\"-Infinity\"".fromJson[Int].isLeft) && + assertTrue("\"NaN\"".fromJson[Int].isLeft) + }, + test("long") { + assert("-123456789012345678".fromJson[Long])(isRight(equalTo(-123456789012345678L))) && + assert("\"-123456789012345678\"".fromJson[Long])(isRight(equalTo(-123456789012345678L))) && + assertTrue("\"Infinity\"".fromJson[Long].isLeft) && + assertTrue("\"-Infinity\"".fromJson[Long].isLeft) && + assertTrue("\"NaN\"".fromJson[Long].isLeft) + }, + test("float") { + assert("-1.234567e9".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && + assert("\"-1.234567e9\"".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && + assert("\"Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && + assert("\"-Infinity\"".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && + assertTrue("\"NaN\"".fromJson[Float].isRight) + }, + test("double") { + assert("-1.23456789012345e9".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && + assert("\"-1.23456789012345e9\"".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && + assert("\"Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && + assert("\"-Infinity\"".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && + assertTrue("\"NaN\"".fromJson[Double].isRight) + }, test("BigDecimal") { - assert("123".fromJson[BigDecimal])(isRight(equalTo(BigDecimal(123)))) + assert("123.0e123".fromJson[BigDecimal])(isRight(equalTo(BigDecimal("123.0e123")))) && + assertTrue("\"Infinity\"".fromJson[BigDecimal].isLeft) && + assertTrue("\"-Infinity\"".fromJson[BigDecimal].isLeft) && + assertTrue("\"NaN\"".fromJson[BigDecimal].isLeft) }, - test("256 bit BigInteger") { - assert("170141183460469231731687303715884105728".fromJson[java.math.BigInteger])( + test("BigInteger") { + assert("170141183460469231731687303715884105728".fromJson[BigInteger])( isRight(equalTo(new BigInteger("170141183460469231731687303715884105728"))) - ) + ) && + assertTrue("\"Infinity\"".fromJson[BigInteger].isLeft) && + assertTrue("\"-Infinity\"".fromJson[BigInteger].isLeft) && + assertTrue("\"NaN\"".fromJson[BigInteger].isLeft) }, test("BigInteger too large") { // this big integer consumes more than 256 bits @@ -56,7 +104,7 @@ object DecoderSpec extends ZIOSpecDefault { }, test("tuples") { assert("""["a",3]""".fromJson[(String, Int)])(isRight(equalTo(("a", 3)))) - assert("""["a","b"]""".fromJson[(String, Int)])(isLeft(equalTo("[1](expected a number, got 'b')"))) + assert("""["a","b"]""".fromJson[(String, Int)])(isLeft(equalTo("[1](expected an Int)"))) assert("""[[0.1,0.2],[0.3,0.4],[-0.3,-]]""".fromJson[Seq[(Double, Double)]])( isLeft(equalTo("[2][1](expected a Double)")) ) From de51096ce54d500bb13e4c75d3fdb6b870f51c58 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 25 Jan 2025 17:01:12 +0100 Subject: [PATCH 095/311] Update Scala 2 to 2.12.20 and 2.13.16 (#1249) --- .github/workflows/ci.yml | 4 ++-- project/BuildHelper.scala | 2 +- .../src/main/scala/zio/json/golden/filehelpers.scala | 10 ++-------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7b8f34ee..698e29c2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: java: ['17', '21'] - scala: ['2.13.15', '3.3.4'] + scala: ['2.13.16', '3.3.4'] steps: - name: Checkout current branch uses: actions/checkout@v4.1.2 @@ -79,7 +79,7 @@ jobs: fail-fast: false matrix: java: ['17', '21'] - scala: ['2.12.19', '2.13.15', '3.3.4'] + scala: ['2.12.20', '2.13.16', '3.3.4'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 5b8643048..71d179ffe 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -238,7 +238,7 @@ object BuildHelper { }, semanticdbEnabled := scalaVersion.value != ScalaDotty, // enable SemanticDB semanticdbOptions += "-P:semanticdb:synthetics:on", - semanticdbVersion := "4.10.2", + semanticdbVersion := "4.12.7", Test / parallelExecution := true, incOptions ~= (_.withLogRecompileOnMacro(false)), autoAPIMappings := true, diff --git a/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala b/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala index 15c1a2243..636b690f3 100644 --- a/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala +++ b/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala @@ -5,8 +5,6 @@ import java.nio.file.Path import zio.{ test => _, _ } import zio.json._ -import zio.stacktracer.TracingImplicits.disableAutoTrace - import java.nio.file.Files object filehelpers { @@ -15,17 +13,13 @@ object filehelpers { if (file.getName == "target") ZIO.succeed(file) else ZIO.attempt(file.getParentFile).flatMap(getRootDir) - def createGoldenDirectory(pathToDir: String)(implicit trace: Trace): Task[Path] = { - val _ = disableAutoTrace // TODO: Find a way to suppress the unused import warning - val rootFile = new File(getClass.getResource("/").toURI) - + def createGoldenDirectory(pathToDir: String)(implicit trace: Trace): Task[Path] = for { - baseFile <- getRootDir(rootFile) + baseFile <- getRootDir(new File(getClass.getResource(".").toURI)) goldenDir = new File(baseFile.getParentFile, pathToDir) path = goldenDir.toPath _ <- ZIO.attemptBlocking(goldenDir.mkdirs) } yield path - } def writeSampleToFile(path: Path, sample: GoldenSample)(implicit trace: Trace): IO[IOException, Unit] = { val jsonString = sample.toJsonPretty From 2a95e027415d99b6f4ecd4dccd05eb7d07f7db53 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:27:45 +0100 Subject: [PATCH 096/311] Update sbt-scalajs, scalajs-compiler, ... to 1.18.2 (#1247) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 9537df73a..94da3c938 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,7 @@ addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.1") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.3") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From 511ef7fd1f6a817559d98e38f08eb3bafd09fc18 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 26 Jan 2025 09:44:17 +0100 Subject: [PATCH 097/311] Use JDK 11 for most CI checks --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 698e29c2a..69d560b99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v4.2.1 with: distribution: temurin - java-version: 21 + java-version: 11 check-latest: true - name: Cache scala dependencies uses: coursier/cache-action@v6 @@ -37,7 +37,7 @@ jobs: strategy: fail-fast: false matrix: - java: ['17', '21'] + java: ['11', '21'] scala: ['2.13.16', '3.3.4'] steps: - name: Checkout current branch @@ -78,7 +78,7 @@ jobs: strategy: fail-fast: false matrix: - java: ['17', '21'] + java: ['11', '21'] scala: ['2.12.20', '2.13.16', '3.3.4'] platform: ['JVM', 'JS', 'Native'] steps: @@ -120,7 +120,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: 21 + java-version: 11 cache: sbt - run: sbt +mimaReportBinaryIssues From 8cde6d61c85fc4f9913d6078e98dc87faf708722 Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:08:19 +0100 Subject: [PATCH 098/311] feat: explicitEmptyCollections V2, for more json (or less) (#1239) --- docs/configuration.md | 37 + .../src/main/scala-2.x/zio/json/macros.scala | 151 +++- .../src/main/scala-3/zio/json/macros.scala | 148 +++- .../zio/json/JsonCodecConfiguration.scala | 60 +- .../src/main/scala/zio/json/JsonDecoder.scala | 171 ++-- .../src/main/scala/zio/json/JsonEncoder.scala | 33 +- .../zio/json/internal/FieldEncoder.scala | 44 + .../scala/zio/json/AnnotationsCodecSpec.scala | 574 ++++++++++-- .../json/ConfigurableDeriveCodecSpec.scala | 834 ++++++++++++++---- .../internal/FieldEncoderHelperSpec.scala | 220 +++++ 10 files changed, 1862 insertions(+), 410 deletions(-) create mode 100644 zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala create mode 100644 zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala diff --git a/docs/configuration.md b/docs/configuration.md index ffcbf6cc0..9e95a60b9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -197,6 +197,43 @@ The following two expressions result in an equal value: The `@jsonAliases` annotation supports multiple aliases. The annotation has no effect on encoding. +## Nulls, explicitNulls + +By default `null` values are omitted from the JSON output. This behavior can be changed by using the `@jsonExplicitNull` annotation on a case class, field or setting `JsonCodecConfiguration.explicitNulls` to `true`. +Missing nulls on decoding are always allowed. + +```scala mdoc +@jsonExplicitNull +case class Mango(ripeness: Option[Int]) + +object Mango { + implicit val codec: JsonCodec[Mango] = DeriveJsonCodec.gen[Mango] +} +``` +The following expression results in a JSON document with a `null` value: +```scala mdoc +Mango(None).toJson +"""{}""".fromJson[Mango] +``` + +## Empty Collections, explicitEmptyCollections + +By default `empty collections` (all supported collection types and case classes) are included from the JSON output an decoding requires empty collections to be present. This behavior can be changed by using the `@jsonExplicitEmptyCollections(encoding = false, decoding = false)` annotation on a case class, field or setting `JsonCodecConfiguration.explicitEmptyCollections` to `ExplicitEmptyCollections(encoding = false, decoding = false)`. The result is that empty collections are omitted from the JSON output and when decoding empty collections are created. It is also possible to have different values for encoding and decoding by using `@jsonExplicitEmptyCollections(encoding = true, decoding = false)` or `@jsonExplicitEmptyCollections(encoding = false, decoding = true)`. + +```scala mdoc +@jsonExplicitEmptyCollections(encoding = false, decoding = false) +case class Pineapple(leaves: List[String]) + +object Pineapple { + implicit val codec: JsonCodec[Pineapple] = DeriveJsonCodec.gen[Pineapple] +} +``` +The following expression results in a JSON document with an empty collection: +```scala mdoc +Pineapple(Nil).toJson +"""{}""".fromJson[Pineapple] +``` + ## @jsonDerive **Requires zio-json-macros** diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index cd1b9ccfe..3b9e62edf 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -4,7 +4,7 @@ import magnolia1._ import zio.Chunk import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.internal.{ Lexer, RetractReader, StringMatrix, Write } +import zio.json.internal.{ FieldEncoder, Lexer, RetractReader, StringMatrix, Write } import scala.annotation._ import scala.language.experimental.macros @@ -24,6 +24,11 @@ final case class jsonAliases(alias: String, aliases: String*) extends Annotation */ final class jsonExplicitNull extends Annotation +/** + * When disabled keys with empty collections will be omitted from the JSON. + */ +final case class jsonExplicitEmptyCollections(encoding: Boolean = true, decoding: Boolean = true) extends Annotation + /** * If used on a sealed class, will determine the name of the field for disambiguating classes. * @@ -211,7 +216,8 @@ object DeriveJsonDecoder { }.isDefined || !config.allowExtraFields if (ctx.parameters.isEmpty) - new JsonDecoder[A] { + new CollectionJsonDecoder[A] { + override def unsafeDecodeMissing(trace: List[JsonError]): A = ctx.rawConstruct(Nil) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { if (no_extra) { @@ -231,7 +237,7 @@ object DeriveJsonDecoder { } } else - new JsonDecoder[A] { + new CollectionJsonDecoder[A] { private[this] val (names, aliases): (Array[String], Array[(String, Int)]) = { val names = new Array[String](ctx.parameters.size) val aliasesBuilder = Array.newBuilder[(String, Int)] @@ -262,11 +268,55 @@ object DeriveJsonDecoder { private[this] val len = names.length private[this] val matrix = new StringMatrix(names, aliases) private[this] val spans = names.map(JsonError.ObjectAccess) - private[this] val defaults = ctx.parameters.map(_.evaluateDefault).toArray + private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap + private[this] val explicitEmptyCollections = + ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.decoding + }.getOrElse(config.explicitEmptyCollections.decoding) + + private[this] val missingValueDecoder = + if (explicitEmptyCollections) { + lazy val missingValueDecoders = tcs.map { d => + if (allowMissingValueDecoder(d)) d + else null + } + (idx: Int, trace: List[JsonError]) => { + val trace_ = spans(idx) :: trace + val decoder = missingValueDecoders(idx) + if (decoder eq null) Lexer.error("missing", trace_) + decoder.unsafeDecodeMissing(trace_) + } + } else { (idx: Int, trace: List[JsonError]) => + tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + } + + @tailrec + private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { + case _: OptionJsonDecoder[_] => true + case _: CollectionJsonDecoder[_] => !explicitEmptyCollections + case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) + case _ => false + } + + override def unsafeDecodeMissing(trace: List[JsonError]): A = { + val ps = new Array[Any](len) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 + } + ctx.rawConstruct(new ArraySeq(ps)) + } + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') @@ -286,12 +336,12 @@ object DeriveJsonDecoder { val default = defaults(idx) ps(idx) = if ( - (default eq None) || in.nextNonWhitespace() != 'n' && { + (default eq null) || in.nextNonWhitespace() != 'n' && { in.retract() true } ) tcs(idx).unsafeDecode(spans(idx) :: trace, in) - else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get() + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() else Lexer.error("expected 'null'", spans(idx) :: trace) } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) @@ -301,8 +351,8 @@ object DeriveJsonDecoder { if (ps(idx) == null) { val default = defaults(idx) ps(idx) = - if (default ne None) default.get() - else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + if (default ne null) default() + else missingValueDecoder(idx, trace) } idx += 1 } @@ -320,7 +370,7 @@ object DeriveJsonDecoder { if (ps(idx) != null) Lexer.error("duplicate", trace) val default = defaults(idx) ps(idx) = - if ((default ne None) && (value eq Json.Null)) default.get() + if ((default ne null) && (value eq Json.Null)) default() else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, value) case _ => if (no_extra) Lexer.error("invalid extra field", trace) @@ -331,8 +381,8 @@ object DeriveJsonDecoder { if (ps(idx) == null) { val default = defaults(idx) ps(idx) = - if (default ne None) default.get() - else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + if (default ne null) default() + else missingValueDecoder(idx, trace) } idx += 1 } @@ -433,6 +483,8 @@ object DeriveJsonEncoder { if (ctx.parameters.isEmpty) new JsonEncoder[A] { + override def isEmpty(a: A): Boolean = true + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = out.write("{}") override final def toJsonAST(a: A): Either[String, Json] = @@ -449,25 +501,34 @@ object DeriveJsonEncoder { private[this] val params = ctx.parameters .filter(p => p.annotations.collectFirst { case _: jsonExclude => () }.isEmpty) .toArray - private[this] val names = - params.map { p => - p.annotations.collectFirst { case jsonField(name) => - name - }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) - } + private[this] val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - private[this] lazy val fields = params.map { - var idx = 0 - p => - val field = ( - p, - names(idx), - p.typeclass.asInstanceOf[JsonEncoder[Any]], - explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - ) - idx += 1 - field + private[this] val explicitEmptyCollections = + ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.encoding + }.getOrElse(config.explicitEmptyCollections.encoding) + + private[this] lazy val fields: Array[FieldEncoder[Any, Param[JsonEncoder, A]]] = params.map { p => + val name = p.annotations.collectFirst { case jsonField(name) => + name + }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) + val withExplicitNulls = explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.encoding + }.getOrElse(explicitEmptyCollections) + new FieldEncoder( + p, + name, + p.typeclass.asInstanceOf[JsonEncoder[Any]], + withExplicitNulls = withExplicitNulls, + withExplicitEmptyCollections = withExplicitEmptyCollections + ) + } + + override def isEmpty(a: A): Boolean = fields.forall { field => + val paramValue = field.p.dereference(a) + field.encoder.isEmpty(paramValue) || field.encoder.isNothing(paramValue) } def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { @@ -479,20 +540,17 @@ object DeriveJsonEncoder { var prevFields = false // whether any fields have been written while (idx < fields.length) { val field = fields(idx) - val p = field._1.dereference(a) - if ({ - val isNothing = field._3.isNothing(p) - !isNothing || field._4 - }) { + val p = field.p.dereference(a) + field.encodeOrSkip(p) { () => // if we have at least one field already, we need a comma if (prevFields) { out.write(',') JsonEncoder.pad(indent_, out) } - JsonEncoder.string.unsafeEncode(field._2, indent_, out) + JsonEncoder.string.unsafeEncode(field.name, indent_, out) if (indent.isEmpty) out.write(':') else out.write(" : ") - field._3.unsafeEncode(p, indent_, out) + field.encoder.unsafeEncode(p, indent_, out) prevFields = true // record that we have at least one field so far } idx += 1 @@ -502,18 +560,17 @@ object DeriveJsonEncoder { } override final def toJsonAST(a: A): Either[String, Json] = - ctx.parameters - .foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, param) => - val name = param.annotations.collectFirst { case jsonField(name) => - name - }.getOrElse(nameTransform(param.label)) - val writeNulls = explicitNulls || param.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - c.flatMap { chunk => - param.typeclass.toJsonAST(param.dereference(a)).map { value => - if (!writeNulls && value == Json.Null) chunk - else chunk :+ name -> value - } - } + fields + .foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, field) => + val param = field.p + val paramValue = field.p.dereference(a).asInstanceOf[param.PType] + field.encodeOrDefault(paramValue)( + () => + c.flatMap { chunk => + param.typeclass.toJsonAST(paramValue).map(value => chunk :+ field.name -> value) + }, + c + ) } .map(Json.Obj.apply) } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 209ee7154..276e5c36a 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -1,6 +1,5 @@ package zio.json -import zio.json.ast.Json import scala.annotation.* import magnolia1.* import scala.deriving.Mirror @@ -10,7 +9,7 @@ import zio.Chunk import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.internal.{ Lexer, RetractReader, StringMatrix, Write } +import zio.json.internal.{ FieldEncoder, Lexer, RetractReader, StringMatrix, Write } import scala.annotation._ import scala.collection.mutable @@ -32,6 +31,11 @@ final case class jsonAliases(alias: String, aliases: String*) extends Annotation */ final class jsonExplicitNull extends Annotation +/** + * When disabled keys with empty collections will be omitted from the JSON. + */ +final case class jsonExplicitEmptyCollections(encoding: Boolean = true, decoding: Boolean = true) extends Annotation + /** * If used on a sealed class, will determine the name of the field for * disambiguating classes. @@ -209,7 +213,7 @@ final class jsonNoExtraFields extends Annotation */ final class jsonExclude extends Annotation -private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) extends JsonDecoder[A] { +private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) extends CollectionJsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { if (no_extra) { Lexer.char(trace, in, '{') @@ -218,6 +222,8 @@ private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A ctx.rawConstruct(Nil) } + override def unsafeDecodeMissing(trace: List[JsonError]): A = ctx.rawConstruct(Nil) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case Json.Obj(_) => ctx.rawConstruct(Nil) @@ -243,7 +249,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (ctx.params.isEmpty) { new CaseObjectDecoder(ctx, no_extra) } else { - new JsonDecoder[A] { + new CollectionJsonDecoder[A] { private val (names, aliases): (Array[String], Array[(String, Int)]) = { val names = Array.ofDim[String](ctx.params.size) val aliasesBuilder = Array.newBuilder[(String, Int)] @@ -278,11 +284,55 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv private val len = names.length private val matrix = new StringMatrix(names, aliases) private val spans = names.map(JsonError.ObjectAccess(_)) - private val defaults = IArray.genericWrapArray(ctx.params.map(_.evaluateDefault)).toArray + private val defaults = IArray.genericWrapArray(ctx.params.map(_.evaluateDefault.orNull)).toArray private lazy val tcs = IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap + private[this] val explicitEmptyCollections = + ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.decoding + }.getOrElse(config.explicitEmptyCollections.decoding) + + private[this] val missingValueDecoder = + if (explicitEmptyCollections) { + lazy val missingValueDecoders = tcs.map { d => + if (allowMissingValueDecoder(d)) d + else null + } + (idx: Int, trace: List[JsonError]) => { + val trace_ = spans(idx) :: trace + val decoder = missingValueDecoders(idx) + if (decoder eq null) Lexer.error("missing", trace_) + decoder.unsafeDecodeMissing(trace_) + } + } else { + (idx: Int, trace: List[JsonError]) => tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + } + + @tailrec + private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { + case _: OptionJsonDecoder[_] => true + case _: CollectionJsonDecoder[_] => !explicitEmptyCollections + case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) + case _ => false + } + + override def unsafeDecodeMissing(trace: List[JsonError]): A = { + val ps = new Array[Any](len) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 + } + ctx.rawConstruct(new ArraySeq(ps)) + } + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') val ps = new Array[Any](len) @@ -292,11 +342,11 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (idx != -1) { if (ps(idx) != null) Lexer.error("duplicate", trace) val default = defaults(idx) - ps(idx) = if ((default eq None) || in.nextNonWhitespace() != 'n' && { + ps(idx) = if ((default eq null) || in.nextNonWhitespace() != 'n' && { in.retract() true }) tcs(idx).unsafeDecode(spans(idx) :: trace, in) - else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default.get() + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() else Lexer.error("expected 'null'", spans(idx) :: trace) } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) @@ -307,8 +357,8 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (ps(idx) == null) { val default = defaults(idx) ps(idx) = - if (default ne None) default.get() - else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + if (default ne null) default() + else missingValueDecoder(idx, trace) } idx += 1 } @@ -325,7 +375,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (ps(idx) != null) Lexer.error("duplicate", trace) val default = defaults(idx) ps(idx) = - if ((default ne None) && (value eq Json.Null)) default.get() + if ((default ne null) && (value eq Json.Null)) default() else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, value) case _ => if (no_extra) Lexer.error("invalid extra field", trace) @@ -336,8 +386,8 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (ps(idx) == null) { val default = defaults(idx) ps(idx) = - if (default ne None) default.get() - else tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + if (default ne null) default() + else missingValueDecoder(idx, trace) } idx += 1 } @@ -466,6 +516,8 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv private lazy val caseObjectEncoder = new JsonEncoder[Any] { + override def isEmpty(a: Any): Boolean = true + def unsafeEncode(a: Any, indent: Option[Int], out: Write): Unit = out.write("{}") @@ -506,19 +558,33 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv case jsonField(name) => name }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) }.toArray + private val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - private lazy val fields = params.map { - var idx = 0 - p => - val field = ( - p, - names(idx), - p.typeclass.asInstanceOf[JsonEncoder[Any]], - explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - ) - idx += 1 - field - }.toArray + private val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.encoding + }.getOrElse(config.explicitEmptyCollections.encoding) + + private[this] lazy val fields: Array[FieldEncoder[Any, CaseClass.Param[JsonEncoder, A]]] = params.map { p => + val name = p.annotations.collectFirst { case jsonField(name) => + name + }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) + val withExplicitNulls = explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.encoding + }.getOrElse(explicitEmptyCollections) + new FieldEncoder( + p, + name, + p.typeclass.asInstanceOf[JsonEncoder[Any]], + withExplicitNulls = withExplicitNulls, + withExplicitEmptyCollections = withExplicitEmptyCollections + ) + } + + override def isEmpty(a: A): Boolean = fields.forall { field => + val paramValue = field.p.deref(a) + field.encoder.isEmpty(paramValue) || field.encoder.isNothing(paramValue) + } def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { out.write('{') @@ -529,20 +595,17 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv var prevFields = false while (idx < fields.length) { val field = fields(idx) - val p = field._1.deref(a) - if ({ - val isNothing = field._3.isNothing(p) - !isNothing || field._4 - }) { + val p = field.p.deref(a) + field.encodeOrSkip(p) { () => // if we have at least one field already, we need a comma if (prevFields) { out.write(',') JsonEncoder.pad(indent_, out) } - JsonEncoder.string.unsafeEncode(field._2, indent_, out) + JsonEncoder.string.unsafeEncode(field.name, indent_, out) if (indent.isEmpty) out.write(':') else out.write(" : ") - field._3.unsafeEncode(p, indent_, out) + field.encoder.unsafeEncode(p, indent_, out) prevFields = true // at least one field so far } idx += 1 @@ -552,18 +615,17 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv } override final def toJsonAST(a: A): Either[String, Json] = { - ctx.params - .foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, param) => - val name = param.annotations.collectFirst { case jsonField(name) => - name - }.getOrElse(nameTransform(param.label)) - val writeNulls = explicitNulls || param.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - c.flatMap { chunk => - param.typeclass.toJsonAST(param.deref(a)).map { value => - if (!writeNulls && value == Json.Null) chunk - else chunk :+ name -> value - } - } + fields + .foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, field) => + val param = field.p + val paramValue = param.deref(a) + field.encodeOrDefault(paramValue)( + () => + c.flatMap { chunk => + param.typeclass.toJsonAST(paramValue).map(value => chunk :+ field.name -> value) + }, + c + ) } .map(Json.Obj.apply) } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala index 9f8428d18..eaa1d512a 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala @@ -3,6 +3,12 @@ package zio.json import zio.json.JsonCodecConfiguration.SumTypeHandling import zio.json.JsonCodecConfiguration.SumTypeHandling.WrapperWithClassNameField +/** + * When disabled for decoding, keys with empty collections will be omitted from the JSON. When disabled for encoding, + * missing keys will default to empty collections. + */ +case class ExplicitEmptyCollections(encoding: Boolean = true, decoding: Boolean = true) + /** * Implicit codec derivation configuration. * @@ -14,33 +20,77 @@ import zio.json.JsonCodecConfiguration.SumTypeHandling.WrapperWithClassNameField * see [[jsonNoExtraFields]] * @param sumTypeMapping * see [[jsonHintNames]] - * @param explicitNulls - * see [[jsonExplicitNull]] */ final case class JsonCodecConfiguration( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, fieldNameMapping: JsonMemberFormat = IdentityFormat, allowExtraFields: Boolean = true, sumTypeMapping: JsonMemberFormat = IdentityFormat, - explicitNulls: Boolean = false + explicitNulls: Boolean = false, + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections() ) { + def this( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = this( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + ExplicitEmptyCollections() + ) def copy( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField.asInstanceOf[SumTypeHandling], fieldNameMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], allowExtraFields: Boolean = true, sumTypeMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], - explicitNulls: Boolean = false + explicitNulls: Boolean = false, + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections() + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections + ) + + def copy( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean ) = new JsonCodecConfiguration( sumTypeHandling, fieldNameMapping, allowExtraFields, sumTypeMapping, - explicitNulls + explicitNulls, + this.explicitEmptyCollections ) } object JsonCodecConfiguration { + def apply( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + ExplicitEmptyCollections() + ) implicit val default: JsonCodecConfiguration = JsonCodecConfiguration() diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 2ca5c4bfb..eec450142 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -135,7 +135,8 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { * Returns a new codec whose decoded values will be mapped by the specified function. */ final def map[B](f: A => B): JsonDecoder[B] = - new JsonDecoder[B] { + new MappedJsonDecoder[B] { + private[json] def underlying: JsonDecoder[A] = self def unsafeDecode(trace: List[JsonError], in: RetractReader): B = f(self.unsafeDecode(trace, in)) @@ -353,7 +354,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with // use a newtype wrapper. implicit def option[A](implicit A: JsonDecoder[A]): JsonDecoder[Option[A]] = - new JsonDecoder[Option[A]] { self => + new OptionJsonDecoder[Option[A]] { self => override def unsafeDecodeMissing(trace: List[JsonError]): Option[A] = None def unsafeDecode(trace: List[JsonError], in: RetractReader): Option[A] = @@ -468,131 +469,176 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with } } +private[json] trait CollectionJsonDecoder[A] extends JsonDecoder[A] +private[json] trait OptionJsonDecoder[A] extends JsonDecoder[A] +private[json] trait MappedJsonDecoder[A] extends JsonDecoder[A] { + private[json] def underlying: JsonDecoder[_] +} + private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { this: JsonDecoder.type => - implicit def array[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[Array[A]] = new JsonDecoder[Array[A]] { + implicit def array[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[Array[A]] = + new CollectionJsonDecoder[Array[A]] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = - builder(trace, in, Array.newBuilder[A]) - } + override def unsafeDecodeMissing(trace: List[JsonError]): Array[A] = Array.empty - implicit def seq[A: JsonDecoder]: JsonDecoder[Seq[A]] = new JsonDecoder[Seq[A]] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = + builder(trace, in, Array.newBuilder[A]) + } - def unsafeDecode(trace: List[JsonError], in: RetractReader): Seq[A] = - builder(trace, in, immutable.Seq.newBuilder[A]) - } + implicit def seq[A: JsonDecoder]: JsonDecoder[Seq[A]] = + new CollectionJsonDecoder[Seq[A]] { - implicit def chunk[A: JsonDecoder]: JsonDecoder[Chunk[A]] = new JsonDecoder[Chunk[A]] { - private[this] val decoder = JsonDecoder[A] - def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[A] = - builder(trace, in, zio.ChunkBuilder.make[A]()) + override def unsafeDecodeMissing(trace: List[JsonError]): Seq[A] = + Seq.empty - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = - json match { - case Json.Arr(elements) => - elements.map { - var i = 0 - json => - val span = JsonError.ArrayAccess(i) - i += 1 - decoder.unsafeFromJsonAST(span :: trace, json) - } - case _ => Lexer.error("Not an array", trace) - } - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Seq[A] = + builder(trace, in, immutable.Seq.newBuilder[A]) + } + + implicit def chunk[A: JsonDecoder]: JsonDecoder[Chunk[A]] = + new CollectionJsonDecoder[Chunk[A]] { + private[this] val decoder = JsonDecoder[A] + + override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[A] = Chunk.empty + + def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[A] = + builder(trace, in, zio.ChunkBuilder.make[A]()) + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = + json match { + case Json.Arr(elements) => + elements.map { + var i = 0 + json => + val span = JsonError.ArrayAccess(i) + i += 1 + decoder.unsafeFromJsonAST(span :: trace, json) + } + case _ => Lexer.error("Not an array", trace) + } + } implicit def nonEmptyChunk[A: JsonDecoder]: JsonDecoder[NonEmptyChunk[A]] = chunk[A].mapOrFail(NonEmptyChunk.fromChunk(_).toRight("Chunk was empty")) implicit def indexedSeq[A: JsonDecoder]: JsonDecoder[IndexedSeq[A]] = - new JsonDecoder[IndexedSeq[A]] { + new CollectionJsonDecoder[IndexedSeq[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): IndexedSeq[A] = IndexedSeq.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): IndexedSeq[A] = builder(trace, in, IndexedSeq.newBuilder[A]) } implicit def linearSeq[A: JsonDecoder]: JsonDecoder[immutable.LinearSeq[A]] = - new JsonDecoder[immutable.LinearSeq[A]] { + new CollectionJsonDecoder[immutable.LinearSeq[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.LinearSeq[A] = + immutable.LinearSeq.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): LinearSeq[A] = builder(trace, in, immutable.LinearSeq.newBuilder[A]) } - implicit def listSet[A: JsonDecoder]: JsonDecoder[immutable.ListSet[A]] = new JsonDecoder[immutable.ListSet[A]] { + implicit def listSet[A: JsonDecoder]: JsonDecoder[immutable.ListSet[A]] = + new CollectionJsonDecoder[immutable.ListSet[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListSet[A] = + immutable.ListSet.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): ListSet[A] = - builder(trace, in, immutable.ListSet.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): ListSet[A] = + builder(trace, in, immutable.ListSet.newBuilder[A]) + } implicit def treeSet[A: JsonDecoder: Ordering]: JsonDecoder[immutable.TreeSet[A]] = - new JsonDecoder[immutable.TreeSet[A]] { + new CollectionJsonDecoder[immutable.TreeSet[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.TreeSet[A] = + immutable.TreeSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): TreeSet[A] = builder(trace, in, immutable.TreeSet.newBuilder[A]) } - implicit def list[A: JsonDecoder]: JsonDecoder[List[A]] = new JsonDecoder[List[A]] { + implicit def list[A: JsonDecoder]: JsonDecoder[List[A]] = + new CollectionJsonDecoder[List[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): List[A] = List.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): List[A] = - builder(trace, in, new mutable.ListBuffer[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): List[A] = + builder(trace, in, new mutable.ListBuffer[A]) + } - implicit def vector[A: JsonDecoder]: JsonDecoder[Vector[A]] = new JsonDecoder[Vector[A]] { + implicit def vector[A: JsonDecoder]: JsonDecoder[Vector[A]] = + new CollectionJsonDecoder[Vector[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): Vector[A] = Vector.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): Vector[A] = - builder(trace, in, immutable.Vector.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Vector[A] = + builder(trace, in, immutable.Vector.newBuilder[A]) + } - implicit def set[A: JsonDecoder]: JsonDecoder[Set[A]] = new JsonDecoder[Set[A]] { + implicit def set[A: JsonDecoder]: JsonDecoder[Set[A]] = + new CollectionJsonDecoder[Set[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): Set[A] = Set.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): Set[A] = - builder(trace, in, Set.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Set[A] = + builder(trace, in, Set.newBuilder[A]) + } - implicit def hashSet[A: JsonDecoder]: JsonDecoder[immutable.HashSet[A]] = new JsonDecoder[immutable.HashSet[A]] { + implicit def hashSet[A: JsonDecoder]: JsonDecoder[immutable.HashSet[A]] = + new CollectionJsonDecoder[immutable.HashSet[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashSet[A] = + immutable.HashSet.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashSet[A] = - builder(trace, in, immutable.HashSet.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashSet[A] = + builder(trace, in, immutable.HashSet.newBuilder[A]) + } implicit def map[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[Map[K, V]] = - new JsonDecoder[Map[K, V]] { + new CollectionJsonDecoder[Map[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): Map[K, V] = Map.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Map[K, V] = keyValueBuilder(trace, in, Map.newBuilder[K, V]) } implicit def hashMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.HashMap[K, V]] = - new JsonDecoder[immutable.HashMap[K, V]] { + new CollectionJsonDecoder[immutable.HashMap[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashMap[K, V] = + immutable.HashMap.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashMap[K, V] = keyValueBuilder(trace, in, immutable.HashMap.newBuilder[K, V]) } implicit def mutableMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[mutable.Map[K, V]] = - new JsonDecoder[mutable.Map[K, V]] { + new CollectionJsonDecoder[mutable.Map[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): mutable.Map[K, V] = + mutable.Map.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): mutable.Map[K, V] = keyValueBuilder(trace, in, mutable.Map.newBuilder[K, V]) } implicit def sortedSet[A: Ordering: JsonDecoder]: JsonDecoder[immutable.SortedSet[A]] = - new JsonDecoder[immutable.SortedSet[A]] { + new CollectionJsonDecoder[immutable.SortedSet[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.SortedSet[A] = + immutable.SortedSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.SortedSet[A] = builder(trace, in, immutable.SortedSet.newBuilder[A]) } implicit def sortedMap[K: JsonFieldDecoder: Ordering, V: JsonDecoder]: JsonDecoder[collection.SortedMap[K, V]] = - new JsonDecoder[collection.SortedMap[K, V]] { + new CollectionJsonDecoder[collection.SortedMap[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): collection.SortedMap[K, V] = + collection.SortedMap.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): collection.SortedMap[K, V] = keyValueBuilder(trace, in, collection.SortedMap.newBuilder[K, V]) } implicit def listMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.ListMap[K, V]] = - new JsonDecoder[immutable.ListMap[K, V]] { + new CollectionJsonDecoder[immutable.ListMap[K, V]] { + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListMap[K, V] = + immutable.ListMap.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.ListMap[K, V] = keyValueBuilder(trace, in, immutable.ListMap.newBuilder[K, V]) @@ -613,18 +659,21 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { private[json] trait DecoderLowPriority2 extends DecoderLowPriority3 { this: JsonDecoder.type => - implicit def iterable[A: JsonDecoder]: JsonDecoder[Iterable[A]] = new JsonDecoder[Iterable[A]] { + implicit def iterable[A: JsonDecoder]: JsonDecoder[Iterable[A]] = + new CollectionJsonDecoder[Iterable[A]] { + override def unsafeDecodeMissing(trace: List[JsonError]): Iterable[A] = Iterable.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): Iterable[A] = - builder(trace, in, immutable.Iterable.newBuilder[A]) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Iterable[A] = + builder(trace, in, immutable.Iterable.newBuilder[A]) + } // not implicit because this overlaps with decoders for lists of tuples def keyValueChunk[K, A](implicit K: JsonFieldDecoder[K], A: JsonDecoder[A] ): JsonDecoder[Chunk[(K, A)]] = - new JsonDecoder[Chunk[(K, A)]] { + new CollectionJsonDecoder[Chunk[(K, A)]] { + override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[(K, A)] = Chunk.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[(K, A)] = keyValueBuilder[K, A, ({ type lambda[X, Y] = Chunk[(X, Y)] })#lambda]( diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 7bed24320..ca09d2977 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -39,6 +39,8 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { override def isNothing(b: B): Boolean = self.isNothing(f(b)) + override def isEmpty(b: B): Boolean = self.isEmpty(f(b)) + override final def toJsonAST(b: B): Either[String, Json] = self.toJsonAST(f(b)) } @@ -78,6 +80,11 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { */ def isNothing(a: A): Boolean = false + /** + * This default may be overridden when this value may be empty within a JSON object and still be encoded. + */ + def isEmpty(a: A): Boolean = false + /** * Returns this encoder but narrowed to the its given sub-type */ @@ -202,6 +209,8 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def isNothing(a: A): Boolean = encoder.isNothing(a) + override def isEmpty(a: A): Boolean = encoder.isEmpty(a) + override def toJsonAST(a: A): Either[String, Json] = encoder.toJsonAST(a) } @@ -311,8 +320,14 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { this: JsonEncoder.type => - implicit def array[A](implicit A: JsonEncoder[A], classTag: ClassTag[A]): JsonEncoder[Array[A]] = + implicit def array[A](implicit + A: JsonEncoder[A], + classTag: ClassTag[A] + ): JsonEncoder[Array[A]] = new JsonEncoder[Array[A]] { + + override def isEmpty(as: Array[A]): Boolean = as.isEmpty + def unsafeEncode(as: Array[A], indent: Option[Int], out: Write): Unit = if (as.isEmpty) out.write("[]") else { @@ -374,9 +389,11 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { implicit def vector[A: JsonEncoder]: JsonEncoder[Vector[A]] = iterable[A, Vector] - implicit def set[A: JsonEncoder]: JsonEncoder[Set[A]] = iterable[A, Set] + implicit def set[A: JsonEncoder]: JsonEncoder[Set[A]] = + iterable[A, Set] - implicit def hashSet[A: JsonEncoder]: JsonEncoder[immutable.HashSet[A]] = iterable[A, immutable.HashSet] + implicit def hashSet[A: JsonEncoder]: JsonEncoder[immutable.HashSet[A]] = + iterable[A, immutable.HashSet] implicit def sortedSet[A: Ordering: JsonEncoder]: JsonEncoder[immutable.SortedSet[A]] = iterable[A, immutable.SortedSet] @@ -400,8 +417,13 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { this: JsonEncoder.type => - implicit def iterable[A, T[X] <: Iterable[X]](implicit A: JsonEncoder[A]): JsonEncoder[T[A]] = + implicit def iterable[A, T[X] <: Iterable[X]](implicit + A: JsonEncoder[A] + ): JsonEncoder[T[A]] = new JsonEncoder[T[A]] { + + override def isEmpty(as: T[A]): Boolean = as.isEmpty + def unsafeEncode(as: T[A], indent: Option[Int], out: Write): Unit = if (as.isEmpty) out.write("[]") else { @@ -449,6 +471,9 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { K: JsonFieldEncoder[K], A: JsonEncoder[A] ): JsonEncoder[T[K, A]] = new JsonEncoder[T[K, A]] { + + override def isEmpty(a: T[K, A]): Boolean = a.isEmpty + def unsafeEncode(kvs: T[K, A], indent: Option[Int], out: Write): Unit = if (kvs.isEmpty) out.write("{}") else { diff --git a/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala b/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala new file mode 100644 index 000000000..ac2cbc34f --- /dev/null +++ b/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala @@ -0,0 +1,44 @@ +package zio.json +package internal + +import zio.Chunk +import zio.json.ast.Json + +private[json] class FieldEncoder[T, P]( + val p: P, + val name: String, + val encoder: JsonEncoder[T], + withExplicitNulls: Boolean, + withExplicitEmptyCollections: Boolean +) { + private[this] val _encodeOrSkip: T => (() => Unit) => Unit = + if (withExplicitNulls && withExplicitEmptyCollections) { _ => encode => + encode() + } else if (withExplicitNulls) { t => encode => + if (!encoder.isEmpty(t)) encode() else () + } else if (withExplicitEmptyCollections) { t => encode => + if (!encoder.isNothing(t)) encode() else () + } else { t => encode => + if (!encoder.isEmpty(t) && !encoder.isNothing(t)) encode() else () + } + def encodeOrSkip(t: T)(encode: () => Unit): Unit = _encodeOrSkip(t)(encode) + + private[this] val _encodeOrDefault: T => ( + Either[String, Chunk[(String, Json)]], + () => Either[String, Chunk[(String, Json)]] + ) => Either[String, Chunk[(String, Json)]] = + if (withExplicitNulls && withExplicitEmptyCollections) { _ => (_, encode) => + encode() + } else if (withExplicitNulls) { t => (default, encode) => + if (!encoder.isEmpty(t)) encode() else default + } else if (withExplicitEmptyCollections) { t => (default, encode) => + if (!encoder.isNothing(t)) encode() else default + } else { t => (default, encode) => + if (!encoder.isEmpty(t) && !encoder.isNothing(t)) encode() else default + } + def encodeOrDefault(t: T)( + encode: () => Either[String, Chunk[(String, Json)]], + default: Either[String, Chunk[(String, Json)]] + ): Either[String, Chunk[(String, Json)]] = + _encodeOrDefault(t)(default, encode) +} diff --git a/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala index e6769d013..373ecd739 100644 --- a/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/AnnotationsCodecSpec.scala @@ -2,148 +2,558 @@ package zio.json import zio.json.ast.Json import zio.test._ +import zio.Chunk + +import scala.collection.immutable +import scala.collection.mutable object AnnotationsCodecSpec extends ZIOSpecDefault { - def spec = suite("ConfigurableDeriveCodecSpec")( + def spec = suite("AnnotationsCodecSpec")( suite("annotations overrides")( - suite("string")( - test("should override field name mapping") { - @jsonMemberNames(SnakeCase) - case class ClassWithFields(someField: Int, someOtherField: String) + test("should override field name mapping") { + @jsonMemberNames(SnakeCase) + case class ClassWithFields(someField: Int, someOtherField: String) + + val expectedStr = """{"some_field":1,"some_other_field":"a"}""" + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should specify discriminator") { + @jsonDiscriminator("$type") + sealed trait ST + + object ST { + case object CaseObj extends ST + case class CaseClass(i: Int) extends ST + + implicit lazy val codec: JsonCodec[ST] = DeriveJsonCodec.gen + } + + val expectedStr = """{"$type":"CaseClass","i":1}""" + val expectedObj: ST = ST.CaseClass(i = 1) + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should override sum type mapping") { + @jsonHintNames(SnakeCase) + @jsonDiscriminator("$type") + sealed trait ST + + object ST { + case object CaseObj extends ST + case class CaseClass(i: Int) extends ST + + implicit lazy val codec: JsonCodec[ST] = DeriveJsonCodec.gen + } + + val expectedStr = """{"$type":"case_class","i":1}""" + val expectedObj: ST = ST.CaseClass(i = 1) + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should prevent extra fields") { + @jsonNoExtraFields + case class ClassWithFields(someField: Int, someOtherField: String) + + val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonStr.fromJson[ClassWithFields].isLeft + ) + }, + test("use explicit null values") { + @jsonExplicitNull + case class OptionalField(a: Option[Int]) + + val expectedStr = """{"a":null}""" + val expectedObj = OptionalField(None) + + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("do not write empty collections") { + @jsonExplicitEmptyCollections(false) + case class EmptySeq(a: Seq[Int]) + + val expectedStr = """{}""" + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(EmptySeq(Seq.empty).toJson == expectedStr) + } + ), + suite("annotations overrides AST")( + test("should override field name mapping") { + @jsonMemberNames(SnakeCase) + case class ClassWithFields(someField: Int, someOtherField: String) + + val expectedAST = Json.Obj("some_field" -> Json.Num(1), "some_other_field" -> Json.Str("a")) + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should specify discriminator") { + @jsonDiscriminator("$type") + sealed trait ST + + object ST { + case object CaseObj extends ST + case class CaseClass(i: Int) extends ST + + implicit lazy val codec: JsonCodec[ST] = DeriveJsonCodec.gen + } + + val expectedAST = Json.Obj("$type" -> Json.Str("CaseClass"), "i" -> Json.Num(1)) + val expectedObj: ST = ST.CaseClass(i = 1) + + assertTrue( + expectedAST.as[ST].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should prevent extra fields") { + @jsonNoExtraFields + case class ClassWithFields(someField: Int, someOtherField: String) + + val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonAST.as[ClassWithFields].isLeft + ) + }, + test("use explicit null values") { + @jsonExplicitNull + case class OptionalField(a: Option[Int]) + + val jsonAST = Json.Obj("a" -> Json.Null) + val expectedObj = OptionalField(None) + + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue(jsonAST.as[OptionalField].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) + }, + test("do not write empty collections") { + @jsonExplicitEmptyCollections(false) + case class EmptySeq(a: Seq[Int]) + + val jsonAST = Json.Obj() + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(EmptySeq(Seq.empty).toJsonAST == Right(jsonAST)) + } + ), + suite("explicit empty collections")( + suite("should fill in missing empty collections and write empty collections")( + test("for an array") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyArray(a: Array[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyArray(Array.empty) + + implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen + + assertTrue("""{}""".fromJson[EmptyArray].toOption.exists(_.a.isEmpty), expectedObj.toJson == expectedStr) + }, + test("for a seq") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptySeq(a: Seq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySeq(Seq.empty) + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - val expectedStr = """{"some_field":1,"some_other_field":"a"}""" - val expectedObj = ClassWithFields(1, "a") + assertTrue("""{}""".fromJson[EmptySeq].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) + }, + test("for a chunk") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyChunk(a: Chunk[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyChunk(Chunk.empty) + + implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen + + assertTrue("""{}""".fromJson[EmptyChunk].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) + }, + test("for an indexed seq") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyIndexedSeq(a: IndexedSeq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen assertTrue( - expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, + """{}""".fromJson[EmptyIndexedSeq].toOption.contains(expectedObj), expectedObj.toJson == expectedStr ) }, - test("should specify discriminator") { - @jsonDiscriminator("$type") - sealed trait ST + test("for a linear seq") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) - object ST { - case object CaseObj extends ST - case class CaseClass(i: Int) extends ST + implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen - implicit lazy val codec: JsonCodec[ST] = DeriveJsonCodec.gen - } + assertTrue( + """{}""".fromJson[EmptyLinearSeq].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr + ) + }, + test("for a list set") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyListSet(a: immutable.ListSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyListSet(immutable.ListSet.empty) - val expectedStr = """{"$type":"CaseClass","i":1}""" - val expectedObj: ST = ST.CaseClass(i = 1) + implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen assertTrue( - expectedStr.fromJson[ST].toOption.get == expectedObj, + """{}""".fromJson[EmptyListSet].toOption.contains(expectedObj), expectedObj.toJson == expectedStr ) }, - test("should override sum type mapping") { - @jsonHintNames(SnakeCase) - @jsonDiscriminator("$type") - sealed trait ST + test("for a tree set") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyTreeSet(a: immutable.TreeSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) - object ST { - case object CaseObj extends ST - case class CaseClass(i: Int) extends ST + implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen - implicit lazy val codec: JsonCodec[ST] = DeriveJsonCodec.gen - } + assertTrue( + """{}""".fromJson[EmptyTreeSet].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr + ) + }, + test("for a list") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyList(a: List[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyList(List.empty) - val expectedStr = """{"$type":"case_class","i":1}""" - val expectedObj: ST = ST.CaseClass(i = 1) + implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen assertTrue( - expectedStr.fromJson[ST].toOption.get == expectedObj, + """{}""".fromJson[EmptyList].toOption.contains(expectedObj), expectedObj.toJson == expectedStr ) }, - test("should prevent extra fields") { - @jsonNoExtraFields - case class ClassWithFields(someField: Int, someOtherField: String) + test("for a vector") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyVector(a: Vector[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyVector(Vector.empty) - val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" - - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen assertTrue( - jsonStr.fromJson[ClassWithFields].isLeft + """{}""".fromJson[EmptyVector].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr ) }, - test("use explicit null values") { - @jsonExplicitNull - case class OptionalField(a: Option[Int]) + test("for a set") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptySet(a: Set[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySet(Set.empty) - val expectedStr = """{"a":null}""" - val expectedObj = OptionalField(None) + implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + assertTrue( + """{}""".fromJson[EmptySet].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr + ) + }, + test("for a hash set") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyHashSet(a: immutable.HashSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyHashSet(immutable.HashSet.empty) + + implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen assertTrue( - expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + """{}""".fromJson[EmptyHashSet].toOption.contains(expectedObj), expectedObj.toJson == expectedStr ) - } - ), - suite("AST")( - test("should override field name mapping") { - @jsonMemberNames(SnakeCase) - case class ClassWithFields(someField: Int, someOtherField: String) + }, + test("for a sorted set") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptySortedSet(a: immutable.SortedSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySortedSet(immutable.SortedSet.empty) - val expectedAST = Json.Obj("some_field" -> Json.Num(1), "some_other_field" -> Json.Str("a")) - val expectedObj = ClassWithFields(1, "a") + implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + assertTrue( + """{}""".fromJson[EmptySortedSet].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr + ) + }, + test("for a map") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyMap(a: Map[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyMap(Map.empty) + + implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen assertTrue( - expectedAST.as[ClassWithFields].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST + """{}""".fromJson[EmptyMap].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr ) }, - test("should specify discriminator") { - @jsonDiscriminator("$type") - sealed trait ST + test("for a hash map") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyHashMap(a: immutable.HashMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyHashMap(immutable.HashMap.empty) - object ST { - case object CaseObj extends ST - case class CaseClass(i: Int) extends ST + implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen - implicit lazy val codec: JsonCodec[ST] = DeriveJsonCodec.gen - } + assertTrue( + """{}""".fromJson[EmptyHashMap].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr + ) + }, + test("for a mutable map") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyMutableMap(a: mutable.Map[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyMutableMap(mutable.Map.empty) - val expectedAST = Json.Obj("$type" -> Json.Str("CaseClass"), "i" -> Json.Num(1)) - val expectedObj: ST = ST.CaseClass(i = 1) + implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen assertTrue( - expectedAST.as[ST].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST + """{}""".fromJson[EmptyMutableMap].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr ) }, - test("should prevent extra fields") { - @jsonNoExtraFields - case class ClassWithFields(someField: Int, someOtherField: String) + test("for a sorted map") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptySortedMap(a: collection.SortedMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptySortedMap(collection.SortedMap.empty) - val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) + implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + assertTrue( + """{}""".fromJson[EmptySortedMap].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr + ) + }, + test("for a list map") { + @jsonExplicitEmptyCollections(true, decoding = false) + case class EmptyListMap(a: immutable.ListMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyListMap(immutable.ListMap.empty) + + implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen assertTrue( - jsonAST.as[ClassWithFields].isLeft + """{}""".fromJson[EmptyListMap].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr ) + } + ), + suite("should not write empty collections and fail missing empty collections")( + test("for an array") { + @jsonExplicitEmptyCollections(false) + case class EmptyArray(a: Array[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyArray(Array.empty) + + implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyArray].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a seq") { + @jsonExplicitEmptyCollections(false) + case class EmptySeq(a: Seq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySeq(Seq.empty) + + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySeq].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a chunk") { + @jsonExplicitEmptyCollections(false) + case class EmptyChunk(a: Chunk[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyChunk(Chunk.empty) + + implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyChunk].isLeft, expectedObj.toJson == expectedStr) + }, + test("for an indexed seq") { + @jsonExplicitEmptyCollections(false) + case class EmptyIndexedSeq(a: IndexedSeq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) + + implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyIndexedSeq].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a linear seq") { + @jsonExplicitEmptyCollections(false) + case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) + + implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyLinearSeq].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a list set") { + @jsonExplicitEmptyCollections(false) + case class EmptyListSet(a: immutable.ListSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyListSet(immutable.ListSet.empty) + + implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListSet].isLeft, expectedObj.toJson == expectedStr) }, - test("use explicit null values") { - @jsonExplicitNull - case class OptionalField(a: Option[Int]) + test("for a treeSet") { + @jsonExplicitEmptyCollections(false) + case class EmptyTreeSet(a: immutable.TreeSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) - val jsonAST = Json.Obj("a" -> Json.Null) - val expectedObj = OptionalField(None) + implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyTreeSet].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a list") { + @jsonExplicitEmptyCollections(false) + case class EmptyList(a: List[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyList(List.empty) + + implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyList].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a vector") { + @jsonExplicitEmptyCollections(false) + case class EmptyVector(a: Vector[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyVector(Vector.empty) + + implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyVector].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a set") { + @jsonExplicitEmptyCollections(false) + case class EmptySet(a: Set[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySet(Set.empty) + + implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySet].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a hash set") { + @jsonExplicitEmptyCollections(false) + case class EmptyHashSet(a: immutable.HashSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyHashSet(immutable.HashSet.empty) + + implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashSet].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a sorted set") { + @jsonExplicitEmptyCollections(false) + case class EmptySortedSet(a: immutable.SortedSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySortedSet(immutable.SortedSet.empty) + + implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySortedSet].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a map") { + @jsonExplicitEmptyCollections(false) + case class EmptyMap(a: Map[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyMap(Map.empty) + + implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyMap].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a hashMap") { + @jsonExplicitEmptyCollections(false) + case class EmptyHashMap(a: immutable.HashMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyHashMap(immutable.HashMap.empty) + + implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashMap].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a mutable map") { + @jsonExplicitEmptyCollections(false) + case class EmptyMutableMap(a: mutable.Map[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyMutableMap(mutable.Map.empty) + + implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyMutableMap].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a sorted map") { + @jsonExplicitEmptyCollections(false) + case class EmptySortedMap(a: collection.SortedMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptySortedMap(collection.SortedMap.empty) + + implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySortedMap].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a list map") { + @jsonExplicitEmptyCollections(false) + case class EmptyListMap(a: immutable.ListMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyListMap(immutable.ListMap.empty) - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen - assertTrue(jsonAST.as[OptionalField].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) + assertTrue(expectedStr.fromJson[EmptyListMap].isLeft, expectedObj.toJson == expectedStr) } ) ) diff --git a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala index 8fc2df6cf..4e8fb105f 100644 --- a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala @@ -3,6 +3,10 @@ package zio.json import zio.json.JsonCodecConfiguration.SumTypeHandling.DiscriminatorField import zio.json.ast.Json import zio.test._ +import zio.Chunk + +import scala.collection.immutable +import scala.collection.mutable object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { case class ClassWithFields(someField: Int, someOtherField: String) @@ -18,259 +22,753 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { def spec = suite("ConfigurableDeriveCodecSpec")( suite("defaults")( - suite("string")( - test("should not map field names by default") { - val expectedStr = """{"someField":1,"someOtherField":"a"}""" - val expectedObj = ClassWithFields(1, "a") + test("should not map field names by default") { + val expectedStr = """{"someField":1,"someOtherField":"a"}""" + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should not use discriminator by default") { + val expectedStr = """{"CaseObj":{}}""" + val expectedObj: ST = ST.CaseObj + + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should allow extra fields by default") { + val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonStr.fromJson[ClassWithFields].toOption.get == expectedObj + ) + }, + test("do not write nulls by default") { + val expectedStr = """{}""" + val expectedObj = OptionalField(None) + + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("do not fail on missing null values") { + val expectedStr = """{}""" + val expectedObj = OptionalField(None) + + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[OptionalField].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + }, + test("write empty collections by default") { + case class Empty(z: Option[Int]) + case class EmptyObj(a: Empty) + case class EmptySeq(a: Seq[Int]) + + val expectedObjStr = """{"a":{}}""" + val expectedSeqStr = """{"a":[]}""" + val expectedObj = EmptyObj(Empty(None)) + val expectedSeq = EmptySeq(Seq.empty) + + implicit val emptyCodec: JsonCodec[Empty] = DeriveJsonCodec.gen + implicit val emptyObjCodec: JsonCodec[EmptyObj] = DeriveJsonCodec.gen + implicit val emptySeqCodec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue( + expectedObjStr.fromJson[EmptyObj].toOption.get == expectedObj, + expectedObj.toJson == expectedObjStr, + expectedSeqStr.fromJson[EmptySeq].toOption.get == expectedSeq, + expectedSeq.toJson == expectedSeqStr + ) + }, + test("fail on decoding missing empty collections by default") { + case class Empty(z: Option[Int]) + case class EmptyObj(a: Empty) + case class EmptySeq(b: Seq[Int]) + + implicit val codecEmpty: JsonCodec[Empty] = DeriveJsonCodec.gen[Empty] + implicit val codecEmptyObj: JsonCodec[EmptyObj] = DeriveJsonCodec.gen[EmptyObj] + implicit val codecEmptySeq: JsonCodec[EmptySeq] = DeriveJsonCodec.gen[EmptySeq] + + assertTrue( + """{}""".fromJson[EmptyObj] == Left(".a(missing)"), + """{}""".fromJson[EmptySeq] == Left(".b(missing)") + ) + } + ), + suite("AST defaults")( + test("should not map field names by default") { + val expectedAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a")) + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should not use discriminator by default") { + val expectedAST = Json.Obj("CaseObj" -> Json.Obj()) + val expectedObj: ST = ST.CaseObj + + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ST].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should allow extra fields by default") { + val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) + val expectedObj = ClassWithFields(1, "a") + + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonAST.as[ClassWithFields].toOption.get == expectedObj + ) + }, + test("do not write nulls by default") { + val jsonAST = Json.Obj() + val expectedObj = OptionalField(None) + + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + jsonAST.as[OptionalField].toOption.get == expectedObj, + expectedObj.toJsonAST == Right(jsonAST) + ) + }, + test("write empty collections by default") { + case class Empty(z: Option[Int]) + case class EmptyObj(a: Empty) + case class EmptySeq(a: Seq[Int]) + + val expectedSeqJson = Json.Obj("a" -> Json.Arr()) + val expectedObjJson = Json.Obj("a" -> Json.Obj()) + val expectedObj = EmptyObj(Empty(None)) + val expectedSeq = EmptySeq(Seq.empty) + + implicit val emptyCodec: JsonCodec[Empty] = DeriveJsonCodec.gen + implicit val emptyObjCodec: JsonCodec[EmptyObj] = DeriveJsonCodec.gen + implicit val emptySeqCodec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue( + expectedObj.toJsonAST == Right(expectedObjJson), + expectedSeq.toJsonAST == Right(expectedSeqJson), + expectedObjJson.as[EmptyObj] == Right(expectedObj), + expectedSeqJson.as[EmptySeq] == Right(expectedSeq) + ) + }, + test("fail on decoding missing empty collections by default") { + case class Empty(z: Option[Int]) + case class EmptyObj(a: Empty) + case class EmptySeq(b: Seq[Int]) + + implicit val codecEmpty: JsonDecoder[Empty] = DeriveJsonDecoder.gen[Empty] + implicit val codecEmptyObj: JsonDecoder[EmptyObj] = DeriveJsonDecoder.gen[EmptyObj] + implicit val codecEmptySeq: JsonDecoder[EmptySeq] = DeriveJsonDecoder.gen[EmptySeq] + + assertTrue( + Json.Obj().as[EmptyObj] == Left(".a(missing)"), + Json.Obj().as[EmptySeq] == Left(".b(missing)") + ) + } + ), + suite("override defaults")( + test("should override field name mapping") { + val expectedStr = """{"some_field":1,"some_other_field":"a"}""" + val expectedObj = ClassWithFields(1, "a") + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(fieldNameMapping = SnakeCase) + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should specify discriminator") { + val expectedStr = """{"$type":"CaseClass","i":1}""" + val expectedObj: ST = ST.CaseClass(i = 1) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type")) + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should override sum type mapping") { + val expectedStr = """{"$type":"case_class","i":1}""" + val expectedObj: ST = ST.CaseClass(i = 1) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type"), sumTypeMapping = SnakeCase) + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[ST].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("should prevent extra fields") { + val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(allowExtraFields = false) + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonStr.fromJson[ClassWithFields].isLeft + ) + }, + test("use explicit null values") { + val expectedStr = """{"a":null}""" + val expectedObj = OptionalField(None) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitNulls = true) + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + expectedObj.toJson == expectedStr + ) + }, + test("do not write empty collections") { + case class Empty(z: Option[Int]) + case class EmptyObj(a: Empty) + case class EmptySeq(b: Seq[Int]) + + val expectedStr = """{}""" + val expectedEmptyObj = EmptyObj(Empty(None)) + val expectedEmptySeq = EmptySeq(Seq.empty) + + implicit val config: JsonCodecConfiguration = JsonCodecConfiguration(explicitEmptyCollections = + ExplicitEmptyCollections(decoding = false, encoding = false) + ) + implicit val codecEmpty: JsonCodec[Empty] = DeriveJsonCodec.gen[Empty] + implicit val codecEmptyObj: JsonCodec[EmptyObj] = DeriveJsonCodec.gen[EmptyObj] + implicit val codecEmptySeq: JsonCodec[EmptySeq] = DeriveJsonCodec.gen[EmptySeq] + + assertTrue( + expectedEmptyObj.toJson == expectedStr, + expectedEmptySeq.toJson == expectedStr, + expectedStr.fromJson[EmptyObj] == Right(expectedEmptyObj), + expectedStr.fromJson[EmptySeq] == Right(expectedEmptySeq) + ) + }, + test("decode missing empty collections with defaults") { + case class EmptySeq(b: Seq[Int] = Seq(1)) + case class EmptyObj(a: EmptySeq) + + val expectedStr = """{}""" + val expectedSeq = EmptySeq(Seq(1)) + val expectedObj = EmptyObj(expectedSeq) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + implicit val codecObj: JsonCodec[EmptyObj] = DeriveJsonCodec.gen + + assertTrue( + expectedStr.fromJson[EmptySeq].toOption.get == expectedSeq, + expectedStr.fromJson[EmptyObj].toOption.get == expectedObj + ) + } + ), + suite("override AST defaults")( + test("should override field name mapping") { + val expectedAST = Json.Obj("some_field" -> Json.Num(1), "some_other_field" -> Json.Str("a")) + val expectedObj = ClassWithFields(1, "a") + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(fieldNameMapping = SnakeCase) + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ClassWithFields].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should specify discriminator") { + val expectedAST = Json.Obj("$type" -> Json.Str("CaseClass"), "i" -> Json.Num(1)) + val expectedObj: ST = ST.CaseClass(i = 1) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type")) + implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + + assertTrue( + expectedAST.as[ST].toOption.get == expectedObj, + expectedObj.toJsonAST.toOption.get == expectedAST + ) + }, + test("should prevent extra fields") { + val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(allowExtraFields = false) + implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + + assertTrue( + jsonAST.as[ClassWithFields].isLeft + ) + }, + test("use explicit null values") { + val jsonAST = Json.Obj("a" -> Json.Null) + val expectedObj = OptionalField(None) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitNulls = true) + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue(jsonAST.as[OptionalField].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) + }, + test("fail on decoding missing explicit nulls") { + val jsonStr = """{}""" + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitNulls = true) + implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + + assertTrue(jsonStr.fromJson[OptionalField].isLeft) + } @@ TestAspect.ignore, + test("do not write empty collections") { + case class Empty(z: Option[Int]) + case class EmptyObj(a: Empty) + case class EmptySeq(b: Seq[Int]) + + val expectedJson = Json.Obj() + val expectedEmptyObj = EmptyObj(Empty(None)) + val expectedEmptySeq = EmptySeq(Seq.empty) + + implicit val config: JsonCodecConfiguration = JsonCodecConfiguration(explicitEmptyCollections = + ExplicitEmptyCollections(decoding = false, encoding = false) + ) + implicit val emptyCodec: JsonCodec[Empty] = DeriveJsonCodec.gen + implicit val emptyObjCodec: JsonCodec[EmptyObj] = DeriveJsonCodec.gen + implicit val emptySeqCodec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + + assertTrue( + expectedEmptyObj.toJsonAST == Right(expectedJson), + expectedEmptySeq.toJsonAST == Right(expectedJson), + expectedJson.as[EmptyObj] == Right(expectedEmptyObj), + expectedJson.as[EmptySeq] == Right(expectedEmptySeq) + ) + } + ), + suite("explicit empty collections")( + suite("should fill in missing empty collections and write empty collections")( + test("for an array") { + case class EmptyArray(a: Array[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyArray(Array.empty) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen - assertTrue( - expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) + assertTrue("""{}""".fromJson[EmptyArray].toOption.exists(_.a.isEmpty), expectedObj.toJson == expectedStr) }, - test("should not use discriminator by default") { - val expectedStr = """{"CaseObj":{}}""" - val expectedObj: ST = ST.CaseObj + test("for a seq") { + case class EmptySeq(a: Seq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySeq(Seq.empty) - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - assertTrue( - expectedStr.fromJson[ST].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) + assertTrue("""{}""".fromJson[EmptySeq].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) + }, + test("for a chunk") { + case class EmptyChunk(a: Chunk[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyChunk(Chunk.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen + + assertTrue("""{}""".fromJson[EmptyChunk].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) }, - test("should allow extra fields by default") { - val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" - val expectedObj = ClassWithFields(1, "a") + test("for an indexed seq") { + case class EmptyIndexedSeq(a: IndexedSeq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen assertTrue( - jsonStr.fromJson[ClassWithFields].toOption.get == expectedObj + """{}""".fromJson[EmptyIndexedSeq].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr ) }, - test("do not write nulls by default, decode missing nulls as None") { - val expectedStr = """{}""" - val expectedObj = OptionalField(None) + test("for a linear seq") { + case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen assertTrue( - expectedStr.fromJson[OptionalField].toOption.get == expectedObj, + """{}""".fromJson[EmptyLinearSeq].toOption.contains(expectedObj), expectedObj.toJson == expectedStr ) }, - test("write empty collections by default") { - case class EmptySeq(a: Seq[Int]) + test("for a list set") { + case class EmptyListSet(a: immutable.ListSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyListSet(immutable.ListSet.empty) + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen + + assertTrue("""{}""".fromJson[EmptyListSet].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) + }, + test("for a tree set") { + case class EmptyTreeSet(a: immutable.TreeSet[Int]) val expectedStr = """{"a":[]}""" - val expectedObj = EmptySeq(Seq.empty) + val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen - assertTrue(expectedStr.fromJson[EmptySeq].toOption.get == expectedObj, expectedObj.toJson == expectedStr) + assertTrue("""{}""".fromJson[EmptyTreeSet].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) }, - test("fail on decoding missing empty collections by default") { - case class Empty(z: Option[Int]) - case class EmptyObj(a: Empty) - case class EmptySeq(a: Seq[Int]) + test("for a list") { + case class EmptyList(a: List[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyList(List.empty) - implicit val codecEmpty: JsonCodec[Empty] = DeriveJsonCodec.gen[Empty] - implicit val codecEmptyObj: JsonCodec[EmptyObj] = DeriveJsonCodec.gen[EmptyObj] - implicit val codecEmptySeq: JsonCodec[EmptySeq] = DeriveJsonCodec.gen[EmptySeq] + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen - assertTrue( - """{}""".fromJson[EmptyObj] == Left(".a(missing)"), - """{}""".fromJson[EmptySeq] == Left(".a(missing)") - ) - } - ), - suite("AST")( - test("should not map field names by default") { - val expectedAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a")) - val expectedObj = ClassWithFields(1, "a") + assertTrue("""{}""".fromJson[EmptyList].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) + }, + test("for a vector") { + case class EmptyVector(a: Vector[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyVector(Vector.empty) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen - assertTrue( - expectedAST.as[ClassWithFields].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST - ) + assertTrue("""{}""".fromJson[EmptyVector].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) }, - test("should not use discriminator by default") { - val expectedAST = Json.Obj("CaseObj" -> Json.Obj()) - val expectedObj: ST = ST.CaseObj + test("for a set") { + case class EmptySet(a: Set[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySet(Set.empty) - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen - assertTrue( - expectedAST.as[ST].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST - ) + assertTrue("""{}""".fromJson[EmptySet].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) }, - test("should allow extra fields by default") { - val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) - val expectedObj = ClassWithFields(1, "a") + test("for a hash set") { + case class EmptyHashSet(a: immutable.HashSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyHashSet(immutable.HashSet.empty) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen - assertTrue( - jsonAST.as[ClassWithFields].toOption.get == expectedObj - ) + assertTrue("""{}""".fromJson[EmptyHashSet].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) }, - test("do not write nulls by default") { - val jsonAST = Json.Obj() - val expectedObj = OptionalField(None) + test("for a sorted set") { + case class EmptySortedSet(a: immutable.SortedSet[Int]) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptySortedSet(immutable.SortedSet.empty) - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen assertTrue( - jsonAST.as[OptionalField].toOption.get == expectedObj, - expectedObj.toJsonAST == Right(jsonAST) + """{}""".fromJson[EmptySortedSet].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr ) }, - test("write empty collections by default") { - case class Empty() - case class EmptySeq(a: Seq[Int], b: Empty) + test("for a map") { + case class EmptyMap(a: Map[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyMap(Map.empty) - val jsonAST = Json.Obj("a" -> Json.Arr(), "b" -> Json.Obj()) - val expectedObj = EmptySeq(Seq.empty, Empty()) + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen - implicit val emptyCodec: JsonCodec[Empty] = DeriveJsonCodec.gen - implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen + assertTrue("""{}""".fromJson[EmptyMap].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) + }, + test("for a hash map") { + case class EmptyHashMap(a: immutable.HashMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyHashMap(immutable.HashMap.empty) - assertTrue( - jsonAST.as[EmptySeq].toOption.get == expectedObj, - expectedObj.toJsonAST == Right(jsonAST) - ) - } - ) - ), - suite("overrides")( - suite("string")( - test("should override field name mapping") { - val expectedStr = """{"some_field":1,"some_other_field":"a"}""" - val expectedObj = ClassWithFields(1, "a") + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen + + assertTrue("""{}""".fromJson[EmptyHashMap].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) + }, + test("for a mutable map") { + case class EmptyMutableMap(a: mutable.Map[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyMutableMap(mutable.Map.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(fieldNameMapping = SnakeCase) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen assertTrue( - expectedStr.fromJson[ClassWithFields].toOption.get == expectedObj, + """{}""".fromJson[EmptyMutableMap].toOption.contains(expectedObj), expectedObj.toJson == expectedStr ) }, - test("should specify discriminator") { - val expectedStr = """{"$type":"CaseClass","i":1}""" - val expectedObj: ST = ST.CaseClass(i = 1) + test("for a sorted map") { + case class EmptySortedMap(a: collection.SortedMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptySortedMap(collection.SortedMap.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type")) - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen assertTrue( - expectedStr.fromJson[ST].toOption.get == expectedObj, + """{}""".fromJson[EmptySortedMap].toOption.contains(expectedObj), expectedObj.toJson == expectedStr ) }, - test("should override sum type mapping") { - val expectedStr = """{"$type":"case_class","i":1}""" - val expectedObj: ST = ST.CaseClass(i = 1) + test("for a list map") { + case class EmptyListMap(a: immutable.ListMap[String, String]) + val expectedStr = """{"a":{}}""" + val expectedObj = EmptyListMap(immutable.ListMap.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type"), sumTypeMapping = SnakeCase) - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen - assertTrue( - expectedStr.fromJson[ST].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) + assertTrue("""{}""".fromJson[EmptyListMap].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) + } + ), + suite("should not write empty collections and fail missing empty collections")( + test("for an array") { + case class EmptyArray(a: Array[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyArray(Array.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyArray] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyArray].isLeft, expectedObj.toJson == expectedStr) }, - test("should prevent extra fields") { - val jsonStr = """{"someField":1,"someOtherField":"a","extra":123}""" + test("for a seq") { + case class EmptySeq(a: Seq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySeq(Seq.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(allowExtraFields = false) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptySeq] = DeriveJsonCodec.gen - assertTrue( - jsonStr.fromJson[ClassWithFields].isLeft - ) + assertTrue(expectedStr.fromJson[EmptySeq].isLeft, expectedObj.toJson == expectedStr) }, - test("use explicit null values") { - val expectedStr = """{"a":null}""" - val expectedObj = OptionalField(None) + test("for a chunk") { + case class EmptyChunk(a: Chunk[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyChunk(Chunk.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitNulls = true) - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyChunk] = DeriveJsonCodec.gen - assertTrue( - expectedStr.fromJson[OptionalField].toOption.get == expectedObj, - expectedObj.toJson == expectedStr - ) - } - ), - suite("AST")( - test("should override field name mapping") { - val expectedAST = Json.Obj("some_field" -> Json.Num(1), "some_other_field" -> Json.Str("a")) - val expectedObj = ClassWithFields(1, "a") + assertTrue(expectedStr.fromJson[EmptyChunk].isLeft, expectedObj.toJson == expectedStr) + }, + test("for an indexed seq") { + case class EmptyIndexedSeq(a: IndexedSeq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyIndexedSeq(IndexedSeq.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(fieldNameMapping = SnakeCase) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyIndexedSeq] = DeriveJsonCodec.gen - assertTrue( - expectedAST.as[ClassWithFields].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST - ) + assertTrue(expectedStr.fromJson[EmptyIndexedSeq].isLeft, expectedObj.toJson == expectedStr) }, - test("should specify discriminator") { - val expectedAST = Json.Obj("$type" -> Json.Str("CaseClass"), "i" -> Json.Num(1)) - val expectedObj: ST = ST.CaseClass(i = 1) + test("for a linear seq") { + case class EmptyLinearSeq(a: immutable.LinearSeq[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyLinearSeq(immutable.LinearSeq.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(sumTypeHandling = DiscriminatorField("$type")) - implicit val codec: JsonCodec[ST] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyLinearSeq] = DeriveJsonCodec.gen - assertTrue( - expectedAST.as[ST].toOption.get == expectedObj, - expectedObj.toJsonAST.toOption.get == expectedAST - ) + assertTrue(expectedStr.fromJson[EmptyLinearSeq].isLeft, expectedObj.toJson == expectedStr) }, - test("should prevent extra fields") { - val jsonAST = Json.Obj("someField" -> Json.Num(1), "someOtherField" -> Json.Str("a"), "extra" -> Json.Num(1)) + test("for a list set") { + case class EmptyListSet(a: immutable.ListSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyListSet(immutable.ListSet.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(allowExtraFields = false) - implicit val codec: JsonCodec[ClassWithFields] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyListSet] = DeriveJsonCodec.gen - assertTrue( - jsonAST.as[ClassWithFields].isLeft - ) + assertTrue(expectedStr.fromJson[EmptyListSet].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a treeSet") { + case class EmptyTreeSet(a: immutable.TreeSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyTreeSet(immutable.TreeSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyTreeSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyTreeSet].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a list") { + case class EmptyList(a: List[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyList(List.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyList] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyList].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a vector") { + case class EmptyVector(a: Vector[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyVector(Vector.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyVector] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyVector].isLeft, expectedObj.toJson == expectedStr) }, - test("use explicit null values") { - val jsonAST = Json.Obj("a" -> Json.Null) - val expectedObj = OptionalField(None) + test("for a set") { + case class EmptySet(a: Set[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySet(Set.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitNulls = true) - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptySet] = DeriveJsonCodec.gen - assertTrue(jsonAST.as[OptionalField].toOption.get == expectedObj, expectedObj.toJsonAST == Right(jsonAST)) + assertTrue(expectedStr.fromJson[EmptySet].isLeft, expectedObj.toJson == expectedStr) }, - test("fail on decoding missing explicit nulls") { - val jsonStr = """{}""" + test("for a hash set") { + case class EmptyHashSet(a: immutable.HashSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptyHashSet(immutable.HashSet.empty) implicit val config: JsonCodecConfiguration = - JsonCodecConfiguration(explicitNulls = true) - implicit val codec: JsonCodec[OptionalField] = DeriveJsonCodec.gen + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyHashSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashSet].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a sorted set") { + case class EmptySortedSet(a: immutable.SortedSet[Int]) + val expectedStr = """{}""" + val expectedObj = EmptySortedSet(immutable.SortedSet.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptySortedSet] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySortedSet].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a map") { + case class EmptyMap(a: Map[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyMap(Map.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyMap].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a hashMap") { + case class EmptyHashMap(a: immutable.HashMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyHashMap(immutable.HashMap.empty) - assertTrue(jsonStr.fromJson[OptionalField].isLeft) - } @@ TestAspect.ignore + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyHashMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyHashMap].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a mutable map") { + case class EmptyMutableMap(a: mutable.Map[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyMutableMap(mutable.Map.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyMutableMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyMutableMap].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a sorted map") { + case class EmptySortedMap(a: collection.SortedMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptySortedMap(collection.SortedMap.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptySortedMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptySortedMap].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a list map") { + case class EmptyListMap(a: immutable.ListMap[String, String]) + val expectedStr = """{}""" + val expectedObj = EmptyListMap(immutable.ListMap.empty) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyListMap].isLeft, expectedObj.toJson == expectedStr) + } ) ) ) diff --git a/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala new file mode 100644 index 000000000..6d5fb6c6c --- /dev/null +++ b/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala @@ -0,0 +1,220 @@ +package zio.json +package internal + +import zio.json.ast.Json +import zio.Chunk +import zio.test._ + +object FieldEncoderSpec extends ZIOSpecDefault { + val spec = suite("FieldEncoder")( + suite("encodeOrSkip")( + suite("OptionEncoder")( + test("should skip encoding None when withExplicitNulls is false") { + val helper = new FieldEncoder( + 1, + "test", + JsonEncoder.option(JsonEncoder.int), + withExplicitNulls = false, + withExplicitEmptyCollections = false + ) + var called = false + helper.encodeOrSkip(None)(() => called = true) + assertTrue(!called) + }, + test("should encode None when withExplicitNulls is true") { + val helper = new FieldEncoder( + 1, + "test", + JsonEncoder.option(JsonEncoder.int), + withExplicitNulls = true, + withExplicitEmptyCollections = false + ) + var called = false + helper.encodeOrSkip(None)(() => called = true) + assertTrue(called) + } + ), + suite("CollectionEncoder")( + suite("for a List")( + test("should encode empty collections when withExplicitEmptyCollections is true") { + val helper = new FieldEncoder( + 1, + "test", + implicitly[JsonEncoder[List[Int]]], + withExplicitNulls = false, + withExplicitEmptyCollections = true + ) + var called = false + helper.encodeOrSkip(Nil)(() => called = true) + assertTrue(called) + }, + test("should not encode empty collections when withExplicitEmptyCollections is false") { + val helper = new FieldEncoder( + 1, + "test", + implicitly[JsonEncoder[List[Int]]], + withExplicitNulls = false, + withExplicitEmptyCollections = false + ) + var called = false + helper.encodeOrSkip(Nil)(() => called = true) + assertTrue(!called) + } + ), + suite("for a case class")( + test("should encode case classes with empty collections when withExplicitEmptyCollections is true") { + case class Test(list: List[Int], option: Option[Int]) + val helper = new FieldEncoder( + 1, + "test", + DeriveJsonEncoder.gen[Test], + withExplicitNulls = false, + withExplicitEmptyCollections = true + ) + var called = false + helper.encodeOrSkip(Test(Nil, None))(() => called = true) + assertTrue(called) + }, + test("should not encode case classes with empty collections when withExplicitEmptyCollections is false") { + case class Test(list: List[Int], option: Option[Int]) + val helper = new FieldEncoder( + 1, + "test", + DeriveJsonEncoder.gen[Test], + withExplicitNulls = false, + withExplicitEmptyCollections = false + ) + var called = false + helper.encodeOrSkip(Test(Nil, None))(() => called = true) + assertTrue(!called) + }, + test( + "should also not encode case classes with empty options when withExplicitEmptyCollections is false, even when withExplicitNulls is true" + ) { + case class Test(list: List[Int], option: Option[Int]) + val helper = new FieldEncoder( + 1, + "test", + DeriveJsonEncoder.gen[Test], + withExplicitNulls = true, + withExplicitEmptyCollections = false + ) + var called = false + helper.encodeOrSkip(Test(Nil, None))(() => called = true) + assertTrue(!called) + } + ) + ) + ), + suite("encodeOrDefault")( + suite("OptionEncoder")( + test("should use the default encoding None when withExplicitNulls is false") { + val helper = new FieldEncoder( + 1, + "test", + JsonEncoder.option(JsonEncoder.int), + withExplicitNulls = false, + withExplicitEmptyCollections = false + ) + val expected = Chunk(("a", Json.Bool.True)) + assertTrue( + helper.encodeOrDefault(None)(() => Left(""), Right(expected)) == Right(expected) + ) + }, + test("should encode None when withExplicitNulls is true") { + val helper = new FieldEncoder( + 1, + "test", + JsonEncoder.option(JsonEncoder.int), + withExplicitNulls = true, + withExplicitEmptyCollections = false + ) + val expected = Chunk(("a", Json.Bool.True)) + assertTrue( + helper.encodeOrDefault(None)(() => Right(expected), Left("")) == Right(expected) + ) + } + ), + suite("CollectionEncoder")( + test("should encode empty collections when withExplicitEmptyCollections is true") { + val helper = new FieldEncoder( + 1, + "test", + implicitly[JsonEncoder[List[Int]]], + withExplicitNulls = false, + withExplicitEmptyCollections = true + ) + val expected = Chunk(("a", Json.Bool.True)) + assertTrue( + helper.encodeOrDefault(Nil)(() => Right(expected), Left("")) == Right(expected) + ) + }, + test("should not encode empty collections when withExplicitEmptyCollections is false") { + val helper = new FieldEncoder( + 1, + "test", + implicitly[JsonEncoder[List[Int]]], + withExplicitNulls = false, + withExplicitEmptyCollections = false + ) + val expected = Chunk(("a", Json.Bool.True)) + assertTrue( + helper.encodeOrDefault(Nil)(() => Left(""), Right(expected)) == Right(expected) + ) + } + ), + suite("for a case class")( + test("should encode case classes with empty collections when withExplicitEmptyCollections is true") { + case class Test(list: List[Int], option: Option[Int]) + val helper = new FieldEncoder( + 1, + "test", + DeriveJsonEncoder.gen[Test], + withExplicitNulls = false, + withExplicitEmptyCollections = true + ) + val expected = Chunk(("a", Json.Bool.True)) + assertTrue( + helper.encodeOrDefault(Test(Nil, None))( + () => Right(expected), + Left("") + ) == Right(expected) + ) + }, + test("should not encode case classes with empty collections when withExplicitEmptyCollections is false") { + case class Test(list: List[Int], option: Option[Int]) + val helper = new FieldEncoder( + 1, + "test", + DeriveJsonEncoder.gen[Test], + withExplicitNulls = false, + withExplicitEmptyCollections = false + ) + val expected = Chunk(("a", Json.Bool.True)) + assertTrue( + helper.encodeOrDefault(Test(Nil, None))(() => Left(""), Right(expected)) == Right(expected) + ) + }, + test( + "should also not encode case classes with empty options when withExplicitEmptyCollections is false, even when withExplicitNulls is true" + ) { + case class Test(list: List[Int], option: Option[Int]) + val helper = new FieldEncoder( + 1, + "test", + DeriveJsonEncoder.gen[Test], + withExplicitNulls = true, + withExplicitEmptyCollections = false + ) + val expected = Chunk(("a", Json.Bool.True)) + assertTrue( + helper.encodeOrDefault(Test(Nil, None))( + () => Left(""), + Right(expected) + ) == Right(expected) + ) + } + ) + ) + ) +} From 7379de6f475f8863f2c8b9156de14840c853c147 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:24:55 +0100 Subject: [PATCH 099/311] Update scalafmt-core to 3.8.6 (#1248) --- .git-blame-ignore-revs | 2 ++ .scalafmt.conf | 2 +- examples/interop-http4s/build.sbt | 4 ++-- .../shared/src/main/scala-3/zio/json/union_derivation.scala | 3 +-- zio-json/shared/src/main/scala/zio/json/ast/ast.scala | 2 -- 5 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..8da422c19 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Scala Steward: Reformat with scalafmt 3.8.6 +35c27463b6f38a27b119cc1d82f2e0e49e335789 diff --git a/.scalafmt.conf b/.scalafmt.conf index 1b52c7a5d..ebf4d16ce 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.8.2" +version = "3.8.6" runner.dialect = scala213 maxColumn = 120 align.preset = most diff --git a/examples/interop-http4s/build.sbt b/examples/interop-http4s/build.sbt index 3287077a3..2cc159389 100644 --- a/examples/interop-http4s/build.sbt +++ b/examples/interop-http4s/build.sbt @@ -3,8 +3,8 @@ val ZioJsonVersion = "0.1.3+8-6eb41b5a-SNAPSHOT" lazy val zioJsonHttp4sExample = (project in file(".")) .settings( - name := "zio-json-http4s-example", - version := "1.0", + name := "zio-json-http4s-example", + version := "1.0", scalaVersion := "2.13.5", scalacOptions ++= Seq("-Xlint:_"), // Only required when using a zio-json snapshot diff --git a/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala index e733473eb..21b05e991 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/union_derivation.scala @@ -56,5 +56,4 @@ private[json] object UnionDerivation: report.errorAndAbort(s"${o.show} is not a subtype of ${bound.show}") transformTypes(tpe).distinct.map(_.asType match - case '[t] => '{ constValue[t] } - ) + case '[t] => '{ constValue[t] }) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index 95e94d372..535003db1 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -205,9 +205,7 @@ sealed abstract class Json { self => /** * - merging objects results in a new objects with all pairs of both sides, with the right hand side being used on * key conflicts - * * - merging arrays results in all of the individual elements being merged - * * - scalar values will be replaced by the right hand side */ final def merge(that: Json): Json = From f614c73a7c5680764f30dcc7a0110e40d86b3c87 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:25:04 +0100 Subject: [PATCH 100/311] Update sbt-scalafmt to 2.5.4 (#1228) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 94da3c938..7c96d8c85 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,7 +7,7 @@ addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.3") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.30") From 7b7aa22f60b081ffcdc3efb861a168e58c5a6fe3 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:25:16 +0100 Subject: [PATCH 101/311] Update snakeyaml-engine to 2.9 (#1230) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 7c96d8c85..8e5bf891d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -12,4 +12,4 @@ addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.30") -libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.8" +libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.9" From b3ff03904e70c6a6390648b43c1a490f694be50c Mon Sep 17 00:00:00 2001 From: Adriani-Furtado <58944585+Adriani-Furtado@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:42:51 +0000 Subject: [PATCH 102/311] Enabled Scala 3 release for http4s interop (#922) --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index 6e6fdd97f..25e31f4c7 100644 --- a/build.sbt +++ b/build.sbt @@ -301,7 +301,6 @@ lazy val zioJsonInteropHttp4s = project .settings(stdSettings("zio-json-interop-http4s")) .settings(buildInfoSettings("zio.json.interop.http4s")) .settings( - crossScalaVersions -= ScalaDotty, libraryDependencies ++= Seq( "org.http4s" %% "http4s-dsl" % "0.23.30", "dev.zio" %% "zio" % zioVersion, From 2e9c951056dc9f94bec4e558832cd1bb30ef83b7 Mon Sep 17 00:00:00 2001 From: Joseph Hajduk Date: Sun, 26 Jan 2025 06:46:59 -0600 Subject: [PATCH 103/311] Fix yaml encoding to work for `Long` values (#941) changed the Json.Num case in jsonToYaml to use BigDecimal.longValue instead of intValue added a test Co-authored-by: josephhajduk --- zio-json-yaml/src/main/scala/zio/json/yaml/package.scala | 2 +- .../src/test/scala/zio/json/yaml/YamlEncoderSpec.scala | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/zio-json-yaml/src/main/scala/zio/json/yaml/package.scala b/zio-json-yaml/src/main/scala/zio/json/yaml/package.scala index a82339fd2..88d3a5517 100644 --- a/zio-json-yaml/src/main/scala/zio/json/yaml/package.scala +++ b/zio-json-yaml/src/main/scala/zio/json/yaml/package.scala @@ -127,7 +127,7 @@ package object yaml { case Json.Num(value) => val stripped = value.stripTrailingZeros() if (stripped.scale() <= 0) { - new ScalarNode(Tag.INT, stripped.intValue().toString, null, null, options.scalarStyle(json)) + new ScalarNode(Tag.INT, stripped.longValue.toString, null, null, options.scalarStyle(json)) } else { new ScalarNode(Tag.FLOAT, stripped.toString, null, null, options.scalarStyle(json)) } diff --git a/zio-json-yaml/src/test/scala/zio/json/yaml/YamlEncoderSpec.scala b/zio-json-yaml/src/test/scala/zio/json/yaml/YamlEncoderSpec.scala index 494a17584..743175005 100644 --- a/zio-json-yaml/src/test/scala/zio/json/yaml/YamlEncoderSpec.scala +++ b/zio-json-yaml/src/test/scala/zio/json/yaml/YamlEncoderSpec.scala @@ -30,6 +30,11 @@ object YamlEncoderSpec extends ZIOSpecDefault { isRight(equalTo("hello\n")) ) }, + test("large number") { + assert(Json.Num(2910000000L).toYaml(YamlOptions.default.copy(lineBreak = LineBreak.UNIX)))( + isRight(equalTo("2910000000\n")) + ) + }, test("special characters in string") { assert(Json.Arr(Json.Str("- [] &hello \\!")).toYaml(YamlOptions.default.copy(lineBreak = LineBreak.UNIX)))( isRight(equalTo(" - '- [] &hello \\!'\n")) From a27121eca9e6f93fa2350d86328a4a73b5f68bb4 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 26 Jan 2025 14:05:55 +0100 Subject: [PATCH 104/311] Temporary disable MiMa checks for zio-json-interop-http4s (#1250) --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 25e31f4c7..b55b3ea27 100644 --- a/build.sbt +++ b/build.sbt @@ -309,7 +309,8 @@ lazy val zioJsonInteropHttp4s = project "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" ), - testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), + mimaPreviousArtifacts := Set() // FIXME: remove after releasing zio-json-interop-http4s for Scala 3 ) .dependsOn(zioJsonJVM) .enablePlugins(BuildInfoPlugin) From 76e2cf019a8b3579802b971e425a4de52f1c5e09 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 26 Jan 2025 14:20:10 +0100 Subject: [PATCH 105/311] Remove CI badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 800551f60..9c7ba3da3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [ZIO Json](https://github.com/zio/zio-json) is a fast and secure JSON library with tight ZIO integration. -[![Production Ready](https://img.shields.io/badge/Project%20Stage-Production%20Ready-brightgreen.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-json/workflows/CI/badge.svg) [![Sonatype Releases](https://img.shields.io/nexus/r/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Release)](https://oss.sonatype.org/content/repositories/releases/dev/zio/zio-json_2.13/) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-json_2.13/) [![javadoc](https://javadoc.io/badge2/dev.zio/zio-json-docs_2.13/javadoc.svg)](https://javadoc.io/doc/dev.zio/zio-json-docs_2.13) [![ZIO JSON](https://img.shields.io/github/stars/zio/zio-json?style=social)](https://github.com/zio/zio-json) +[![Production Ready](https://img.shields.io/badge/Project%20Stage-Production%20Ready-brightgreen.svg)](https://github.com/zio/zio/wiki/Project-Stages) [![Sonatype Releases](https://img.shields.io/nexus/r/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Release)](https://oss.sonatype.org/content/repositories/releases/dev/zio/zio-json_2.13/) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-json_2.13.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-json_2.13/) [![javadoc](https://javadoc.io/badge2/dev.zio/zio-json-docs_2.13/javadoc.svg)](https://javadoc.io/doc/dev.zio/zio-json-docs_2.13) [![ZIO JSON](https://img.shields.io/github/stars/zio/zio-json?style=social)](https://github.com/zio/zio-json) ## Introduction From 96119fe84a87be14b7a2846c76214b71c9e89faf Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Sun, 26 Jan 2025 14:47:03 +0100 Subject: [PATCH 106/311] Add string-like field decoder (#1137) --- .../src/main/scala/zio/json/JsonFieldDecoder.scala | 12 +++++++++++- .../test/scala-3/zio/json/DerivedDecoderSpec.scala | 6 ++++++ .../test/scala-3/zio/json/DerivedEncoderSpec.scala | 5 +++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala index f4336394a..a65f13983 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala @@ -42,7 +42,7 @@ trait JsonFieldDecoder[+A] { def unsafeDecodeField(trace: List[JsonError], in: String): A } -object JsonFieldDecoder { +object JsonFieldDecoder extends LowPriorityJsonFieldDecoder { def apply[A](implicit a: JsonFieldDecoder[A]): JsonFieldDecoder[A] = a implicit val string: JsonFieldDecoder[String] = new JsonFieldDecoder[String] { @@ -87,3 +87,13 @@ object JsonFieldDecoder { if (s.length <= len) s else s.substring(0, len) + "..." } + +private[json] trait LowPriorityJsonFieldDecoder { + + def string: JsonFieldDecoder[String] + + private def quotedString = string.map(raw => s""""$raw"""") + + implicit def stringLike[T <: String: JsonDecoder]: JsonFieldDecoder[T] = + quotedString.mapOrFail(implicitly[JsonDecoder[T]].decodeJson) +} diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala index 96001af75..4e13b783a 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala @@ -80,6 +80,12 @@ object DerivedDecoderSpec extends ZIOSpecDefault { assertTrue("""{"aOrB": "A", "optA": "A"}""".fromJson[Foo] == Right(Foo("A", Some("A")))) && assertTrue("""{"aOrB": "C"}""".fromJson[Foo] == Left(".aOrB(expected one of: A, B)")) + }, + test("Derives and decodes for a custom map key string-based union type") { + case class Foo(aOrB: Map["A" | "B", Int]) derives JsonDecoder + + assertTrue("""{"aOrB": {"A": 1, "B": 2}}""".fromJson[Foo] == Right(Foo(Map("A" -> 1, "B" -> 2)))) && + assertTrue("""{"aOrB": {"C": 1}}""".fromJson[Foo] == Left(".aOrB.C((expected one of: A, B))")) } ) } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala index 1c10fd299..05430a2c9 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala @@ -59,6 +59,11 @@ object DerivedEncoderSpec extends ZIOSpecDefault { case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonEncoder assertTrue(Foo("A", Some("A")).toJson == """{"aOrB":"A","optA":"A"}""") + }, + test("Derives and encodes for a custom map key string-based union type") { + case class Foo(aOrB: Map["A" | "B", Int]) derives JsonEncoder + + assertTrue(Foo(Map("A" -> 1, "B" -> 2)).toJson == """{"aOrB":{"A":1,"B":2}}""") } ) } From 2a8edfb2aede8316480f8fdd88372135403f4f20 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 26 Jan 2025 15:43:37 +0100 Subject: [PATCH 107/311] More efficient field decoder implementation for string-like values (#1251) * More efficient field decoder implementation for string-like values * Fix error messages for field decoder implementation of string-like values --- .../src/main/scala/zio/json/JsonFieldDecoder.scala | 13 ++++++------- .../test/scala-3/zio/json/DerivedDecoderSpec.scala | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala index a65f13983..f5cfdd2dc 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala @@ -15,7 +15,7 @@ */ package zio.json -import zio.json.internal.Lexer +import zio.json.internal.{ FastStringReader, Lexer } import zio.json.uuid.UUIDParser /** When decoding a JSON Object, we only allow the keys that implement this interface. */ @@ -90,10 +90,9 @@ object JsonFieldDecoder extends LowPriorityJsonFieldDecoder { private[json] trait LowPriorityJsonFieldDecoder { - def string: JsonFieldDecoder[String] - - private def quotedString = string.map(raw => s""""$raw"""") - - implicit def stringLike[T <: String: JsonDecoder]: JsonFieldDecoder[T] = - quotedString.mapOrFail(implicitly[JsonDecoder[T]].decodeJson) + implicit def stringLike[T <: String](implicit decoder: JsonDecoder[T]): JsonFieldDecoder[T] = + new JsonFieldDecoder[T] { + def unsafeDecodeField(trace: List[JsonError], in: String): T = + decoder.unsafeDecode(trace, new FastStringReader('"' + in + '"')) + } } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala index 4e13b783a..0007bf618 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala @@ -85,7 +85,7 @@ object DerivedDecoderSpec extends ZIOSpecDefault { case class Foo(aOrB: Map["A" | "B", Int]) derives JsonDecoder assertTrue("""{"aOrB": {"A": 1, "B": 2}}""".fromJson[Foo] == Right(Foo(Map("A" -> 1, "B" -> 2)))) && - assertTrue("""{"aOrB": {"C": 1}}""".fromJson[Foo] == Left(".aOrB.C((expected one of: A, B))")) + assertTrue("""{"aOrB": {"C": 1}}""".fromJson[Foo] == Left(".aOrB.C(expected one of: A, B)")) } ) } From 09802ef814a044a52f4d14f13126e02da8011282 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 27 Jan 2025 07:46:38 +0100 Subject: [PATCH 108/311] Fix NPE for zio-json-golden (#1253) * Fix NPE for zio-json-golden * Add zio-json-golden example project --- examples/zio-json-golden/build.sbt | 2 ++ examples/zio-json-golden/project/build.properties | 1 + .../src/test/scala/EncodeDecodeSpec.scala | 15 +++++++++++++++ .../main/scala/zio/json/golden/filehelpers.scala | 10 ++++++++-- 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 examples/zio-json-golden/build.sbt create mode 100644 examples/zio-json-golden/project/build.properties create mode 100644 examples/zio-json-golden/src/test/scala/EncodeDecodeSpec.scala diff --git a/examples/zio-json-golden/build.sbt b/examples/zio-json-golden/build.sbt new file mode 100644 index 000000000..bb46c21bb --- /dev/null +++ b/examples/zio-json-golden/build.sbt @@ -0,0 +1,2 @@ +scalaVersion := "2.13.16" +libraryDependencies += "dev.zio" %% "zio-json-golden" % "0.7.7" diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties new file mode 100644 index 000000000..73df629ac --- /dev/null +++ b/examples/zio-json-golden/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.7 diff --git a/examples/zio-json-golden/src/test/scala/EncodeDecodeSpec.scala b/examples/zio-json-golden/src/test/scala/EncodeDecodeSpec.scala new file mode 100644 index 000000000..06bf26683 --- /dev/null +++ b/examples/zio-json-golden/src/test/scala/EncodeDecodeSpec.scala @@ -0,0 +1,15 @@ +import zio.json._ +import zio.json.golden._ +import zio.test._ +import zio.test.magnolia.DeriveGen + +object EncodeDecodeSpec extends ZIOSpecDefault { + case class Banana(curvature: Double) + object Banana { + implicit val codec: JsonCodec[Banana] = DeriveJsonCodec.gen[Banana] + } + + def spec = suite("EncodeDecodeSpec")( + goldenTest(DeriveGen[Banana]) + ) +} diff --git a/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala b/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala index 636b690f3..db677d410 100644 --- a/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala +++ b/zio-json-golden/src/main/scala/zio/json/golden/filehelpers.scala @@ -13,13 +13,19 @@ object filehelpers { if (file.getName == "target") ZIO.succeed(file) else ZIO.attempt(file.getParentFile).flatMap(getRootDir) - def createGoldenDirectory(pathToDir: String)(implicit trace: Trace): Task[Path] = + def createGoldenDirectory(pathToDir: String)(implicit trace: Trace): Task[Path] = { + val rootFile = + try new File(getClass.getResource("/").toURI) + catch { // fixes "java.lang.IllegalArgumentException: URI is not hierarchical" in unit tests with Scala 2.12.20 + case _: IllegalArgumentException => new File(getClass.getResource(".").toURI) + } for { - baseFile <- getRootDir(new File(getClass.getResource(".").toURI)) + baseFile <- getRootDir(rootFile) goldenDir = new File(baseFile.getParentFile, pathToDir) path = goldenDir.toPath _ <- ZIO.attemptBlocking(goldenDir.mkdirs) } yield path + } def writeSampleToFile(path: Path, sample: GoldenSample)(implicit trace: Trace): IO[IOException, Unit] = { val jsonString = sample.toJsonPretty From 1b84e28581792312031bd8bbaab14771c65e11dd Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 27 Jan 2025 09:28:02 +0100 Subject: [PATCH 109/311] Add version scheme to published `pom.xml` files (#1254) --- project/BuildHelper.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 71d179ffe..5de7f2eef 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -236,6 +236,7 @@ object BuildHelper { compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.3" cross CrossVersion.full) ) }, + versionScheme := Some("early-semver"), semanticdbEnabled := scalaVersion.value != ScalaDotty, // enable SemanticDB semanticdbOptions += "-P:semanticdb:synthetics:on", semanticdbVersion := "4.12.7", From febd0e5541c9e0c8b6ff46a1ef04040c6e0235d2 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 27 Jan 2025 09:29:34 +0100 Subject: [PATCH 110/311] Update zio-json-golden to 0.7.8 in examples (#1255) --- examples/zio-json-golden/build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/zio-json-golden/build.sbt b/examples/zio-json-golden/build.sbt index bb46c21bb..6eb909e21 100644 --- a/examples/zio-json-golden/build.sbt +++ b/examples/zio-json-golden/build.sbt @@ -1,2 +1,2 @@ scalaVersion := "2.13.16" -libraryDependencies += "dev.zio" %% "zio-json-golden" % "0.7.7" +libraryDependencies += "dev.zio" %% "zio-json-golden" % "0.7.8" From 2867a7463fd3422debc392cb93c6a2b71d8dd70b Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 27 Jan 2025 13:45:52 +0100 Subject: [PATCH 111/311] More efficient parsing of numbers (#1256) * More efficient parsing of numbers * Fix compilation warning * Fix wrong filtering in tests --- .../scala/zio/json/JsonFieldDecoder.scala | 2 +- .../scala/zio/json/internal/numbers.scala | 202 +++++++----------- .../src/test/scala/zio/json/CodecSpec.scala | 1 - .../zio/json/internal/StringMatrixSpec.scala | 2 +- 4 files changed, 84 insertions(+), 123 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala index f5cfdd2dc..83e2d57ec 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala @@ -93,6 +93,6 @@ private[json] trait LowPriorityJsonFieldDecoder { implicit def stringLike[T <: String](implicit decoder: JsonDecoder[T]): JsonFieldDecoder[T] = new JsonFieldDecoder[T] { def unsafeDecodeField(trace: List[JsonError], in: String): T = - decoder.unsafeDecode(trace, new FastStringReader('"' + in + '"')) + decoder.unsafeDecode(trace, new FastStringReader(s""""$in"""")) } } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala index 979cb838f..f184293b0 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala @@ -306,7 +306,7 @@ object SafeNumbers { // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt - private final val offsets = Array( + private[this] val offsets = Array( 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 4889916394579099648L, 4889916394579099648L, 4889916394579099648L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, @@ -665,17 +665,17 @@ object UnsafeNumbers { def byte(num: String): Byte = byte_(new FastStringReader(num), true) def byte_(in: Reader, consume: Boolean): Byte = - long__(in, Byte.MinValue, Byte.MaxValue, consume).toByte + int__(in, -128, 127, consume).toByte def short(num: String): Short = short_(new FastStringReader(num), true) def short_(in: Reader, consume: Boolean): Short = - long__(in, Short.MinValue, Short.MaxValue, consume).toShort + int__(in, -32768, 32767, consume).toShort def int(num: String): Int = int_(new FastStringReader(num), true) def int_(in: Reader, consume: Boolean): Int = - long__(in, Int.MinValue, Int.MaxValue, consume).toInt + int__(in, -2147483648, 2147483647, consume).toInt def long(num: String): Long = long_(new FastStringReader(num), true) @@ -690,115 +690,88 @@ object UnsafeNumbers { max_bits: Int ): java.math.BigInteger = { var current: Int = in.read() - var negative = false - - if (current == '-') { - negative = true - current = in.read() - } else if (current == '+') - current = in.read() + val negative = current == '-' + if (negative || current == '+') current = in.read() if (current == -1) throw UnsafeNumber - bigDecimal__(in, consume, negative, current, true, max_bits).unscaledValue } - // measured faster than Character.isDigit - @inline private[this] def isDigit(i: Int): Boolean = - '0' <= i && i <= '9' - - // is it worth keeping this custom long__ instead of using bigInteger since it - // is approximately double the performance. - def long__(in: Reader, lower: Long, upper: Long, consume: Boolean): Long = { - var current: Int = 0 - - current = in.read() - if (current == -1) throw UnsafeNumber - var negative = false - if (current == '-') { - negative = true - current = in.read() - if (current == -1) throw UnsafeNumber - } else if (current == '+') { + def int__(in: Reader, lower: Int, upper: Int, consume: Boolean): Int = { + var current = in.read() + val negative = current == '-' + if (negative || current == '+') current = in.read() + if (current < '0' || current > '9') throw UnsafeNumber + var accum = '0' - current + while ({ current = in.read() - if (current == -1) throw UnsafeNumber + '0' <= current && current <= '9' + }) { + if ( + accum < -214748364 || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber } + if (consume && current != -1) throw UnsafeNumber + if (negative) { + if (accum < lower) throw UnsafeNumber + } else if (accum != -2147483648) { + accum = -accum + if (upper < accum) throw UnsafeNumber + } else throw UnsafeNumber + accum + } - if (!isDigit(current)) - throw UnsafeNumber - - var accum: Long = 0L + def long__(in: Reader, lower: Long, upper: Long, consume: Boolean): Long = { + var current = in.read() + val negative = current == '-' + if (negative || current == '+') current = in.read() + if (current < '0' || current > '9') throw UnsafeNumber + var accum = ('0' - current).toLong while ({ - { - val c = current - '0' - if (accum <= longunderflow) - if (accum < longunderflow) - throw UnsafeNumber - else if (accum == longunderflow && c == 9) - throw UnsafeNumber - // count down, not up, because it is larger - accum = accum * 10 - c // should never underflow - current = in.read() - }; current != -1 && isDigit(current) - }) () - + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -922337203685477580L || { + accum = (accum << 3) + (accum << 1) + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } if (consume && current != -1) throw UnsafeNumber - - if (negative) - if (accum < lower || upper < accum) throw UnsafeNumber - else accum - else if (accum == Long.MinValue) - throw UnsafeNumber - else { + if (negative) { + if (accum < lower) throw UnsafeNumber + } else if (accum != -9223372036854775808L) { accum = -accum - if (accum < lower || upper < accum) throw UnsafeNumber - else accum - } + if (upper < accum) throw UnsafeNumber + } else throw UnsafeNumber + accum } def float(num: String, max_bits: Int): Float = float_(new FastStringReader(num), true, max_bits) def float_(in: Reader, consume: Boolean, max_bits: Int): Float = { - var current: Int = in.read() - var negative = false - - def readAll(s: String): Unit = { - var i = 0 - val len = s.length - - while (i < len) { - current = in.read() - if (current != s(i)) throw UnsafeNumber - i += 1 - } - - current = in.read() // to be consistent read the terminator - - if (consume && current != -1) - throw UnsafeNumber - } + var current = in.read() + var negative = false if (current == 'N') { - readAll("aN") + readAll(in, "aN", consume) return Float.NaN } - if (current == '-') { - negative = true - current = in.read() - } else if (current == '+') { - current = in.read() - } + negative = current == '-' + if (negative || current == '+') current = in.read() if (current == 'I') { - readAll("nfinity") - + readAll(in, "nfinity", consume) if (negative) return Float.NegativeInfinity else return Float.PositiveInfinity } - if (current == -1) - throw UnsafeNumber + if (current == -1) throw UnsafeNumber val res = bigDecimal__(in, consume, negative = negative, initial = current, int_only = false, max_bits = max_bits) @@ -810,34 +783,19 @@ object UnsafeNumbers { double_(new FastStringReader(num), true, max_bits) def double_(in: Reader, consume: Boolean, max_bits: Int): Double = { - var current: Int = in.read() - var negative = false - - def readall(s: String): Unit = { - var i = 0 - val len = s.length - while (i < len) { - current = in.read() - if (current != s(i)) throw UnsafeNumber - i += 1 - } - current = in.read() // to be consistent read the terminator - if (consume && current != -1) throw UnsafeNumber - } + var current = in.read() + var negative = false if (current == 'N') { - readall("aN") + readAll(in, "aN", consume) return Double.NaN } - if (current == '-') { - negative = true - current = in.read() - } else if (current == '+') - current = in.read() + negative = current == '-' + if (negative || current == '+') current = in.read() if (current == 'I') { - readall("nfinity") + readAll(in, "nfinity", consume) if (negative) return Double.NegativeInfinity else return Double.PositiveInfinity } @@ -860,6 +818,18 @@ object UnsafeNumbers { else res.doubleValue } + private[this] def readAll(in: Reader, s: String, consume: Boolean): Unit = { + val len = s.length + var i, current = 0 + while (i < len) { + current = in.read() + if (current != s(i)) throw UnsafeNumber + i += 1 + } + current = in.read() // to be consistent read the terminator + if (consume && current != -1) throw UnsafeNumber + } + def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = bigDecimal_(new FastStringReader(num), true, max_bits) def bigDecimal_( @@ -868,15 +838,9 @@ object UnsafeNumbers { max_bits: Int ): java.math.BigDecimal = { var current: Int = in.read() - var negative = false - - if (current == '-') { - negative = true - current = in.read() - } else if (current == '+') - current = in.read() + val negative = current == '-' + if (negative || current == '+') current = in.read() if (current == -1) throw UnsafeNumber - bigDecimal__(in, consume, negative, current, false, max_bits) } @@ -917,7 +881,7 @@ object UnsafeNumbers { // arbitrary limit on BigInteger size to avoid OOM attacks if (sig_.bitLength >= max_bits) throw UnsafeNumber - } else if (sig >= longoverflow) + } else if (sig >= 922337203685477580L) sig_ = java.math.BigInteger .valueOf(sig) .multiply(java.math.BigInteger.TEN) @@ -937,7 +901,7 @@ object UnsafeNumbers { if (negative) res.negate else res } - while (isDigit(current)) { + while ('0' <= current && current <= '9') { push_sig() if (!advance()) return significand() @@ -953,7 +917,7 @@ object UnsafeNumbers { if (sig < 0) sig = 0 // e.g. ".1" is shorthand for "0.1" if (!advance()) return significand() - while (isDigit(current)) { + while ('0' <= current && current <= '9') { dot += 1 if (sig > 0 || current != '0') push_sig() @@ -980,6 +944,4 @@ object UnsafeNumbers { // note that bigDecimal does not have a negative zero private[this] val bigIntegers: Array[java.math.BigInteger] = (0L to 9L).map(java.math.BigInteger.valueOf).toArray - private[this] val longunderflow: Long = Long.MinValue / 10L - private[this] val longoverflow: Long = Long.MaxValue / 10L } diff --git a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala index 13a295ff4..2d7cbe7dd 100644 --- a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala @@ -35,7 +35,6 @@ object CodecSpec extends ZIOSpecDefault { ) }, test("primitives") { - val exampleBDString = "234234.234" // this big integer consumes more than 256 bits assert( "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" diff --git a/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala index b3ce953a5..8553f2b32 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala @@ -23,7 +23,7 @@ object StringMatrixSpec extends ZIOSpecDefault { } }, test("negative fails") { - check(genTestStrings.filterNot(_.startsWith("wibble")))(xs => + check(genTestStrings.filter(_.forall(s => !s.startsWith("wibble"))))(xs => assert(matcher(xs, Array.empty, "wibble").toVector)(isEmpty) ) }, From 23db2c19f2439a0545602386dbe617ec9c6fce58 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 27 Jan 2025 15:17:28 +0100 Subject: [PATCH 112/311] Fix to throw an error when parsing numeric values that starts from '+' (#1257) --- .../scala/zio/json/internal/numbers.scala | 67 +++++++++++++------ .../src/test/scala/zio/json/DecoderSpec.scala | 31 ++++++++- .../shared/src/test/scala/zio/json/Gens.scala | 2 +- 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala index f184293b0..a14c51dc2 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala @@ -691,7 +691,7 @@ object UnsafeNumbers { ): java.math.BigInteger = { var current: Int = in.read() val negative = current == '-' - if (negative || current == '+') current = in.read() + if (negative) current = in.read() if (current == -1) throw UnsafeNumber bigDecimal__(in, consume, negative, current, true, max_bits).unscaledValue } @@ -699,7 +699,7 @@ object UnsafeNumbers { def int__(in: Reader, lower: Int, upper: Int, consume: Boolean): Int = { var current = in.read() val negative = current == '-' - if (negative || current == '+') current = in.read() + if (negative) current = in.read() if (current < '0' || current > '9') throw UnsafeNumber var accum = '0' - current while ({ @@ -726,7 +726,7 @@ object UnsafeNumbers { def long__(in: Reader, lower: Long, upper: Long, consume: Boolean): Long = { var current = in.read() val negative = current == '-' - if (negative || current == '+') current = in.read() + if (negative) current = in.read() if (current < '0' || current > '9') throw UnsafeNumber var accum = ('0' - current).toLong while ({ @@ -763,9 +763,13 @@ object UnsafeNumbers { } negative = current == '-' - if (negative || current == '+') current = in.read() + if (negative) current = in.read() - if (current == 'I') { + if (current == 'I' || current == '+') { + if (current == '+') { + current = in.read() + if (current != 'I') throw UnsafeNumber + } readAll(in, "nfinity", consume) if (negative) return Float.NegativeInfinity else return Float.PositiveInfinity @@ -792,9 +796,13 @@ object UnsafeNumbers { } negative = current == '-' - if (negative || current == '+') current = in.read() + if (negative) current = in.read() - if (current == 'I') { + if (current == 'I' || current == '+') { + if (current == '+') { + current = in.read() + if (current != 'I') throw UnsafeNumber + } readAll(in, "nfinity", consume) if (negative) return Double.NegativeInfinity else return Double.PositiveInfinity @@ -839,7 +847,7 @@ object UnsafeNumbers { ): java.math.BigDecimal = { var current: Int = in.read() val negative = current == '-' - if (negative || current == '+') current = in.read() + if (negative) current = in.read() if (current == -1) throw UnsafeNumber bigDecimal__(in, consume, negative, current, false, max_bits) } @@ -859,15 +867,11 @@ object UnsafeNumbers { var dot: Int = 0 // counts from the right var exp: Int = 0 // implied - def advance(): Boolean = { - current = in.read() - current != -1 - } - // skip trailing zero on the left while (current == '0') { sig = 0 - if (!advance()) + current = in.read() + if (current == -1) return java.math.BigDecimal.ZERO } @@ -903,7 +907,8 @@ object UnsafeNumbers { while ('0' <= current && current <= '9') { push_sig() - if (!advance()) + current = in.read() + if (current == -1) return significand() } @@ -915,7 +920,8 @@ object UnsafeNumbers { if (current == '.') { if (sig < 0) sig = 0 // e.g. ".1" is shorthand for "0.1" - if (!advance()) + current = in.read() + if (current == -1) return significand() while ('0' <= current && current <= '9') { dot += 1 @@ -923,15 +929,36 @@ object UnsafeNumbers { push_sig() // overflowed... if (dot < 0) throw UnsafeNumber - advance() + current = in.read() } } if (sig < 0) throw UnsafeNumber // no significand - if (current == 'E' || current == 'e') - exp = int_(in, consume) - else if (consume && current != -1) + if (current == 'E' || current == 'e') { + current = in.read() + val negative = current == '-' + if (negative || current == '+') current = in.read() + if (current < '0' || current > '9') throw UnsafeNumber + var accum = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -214748364 || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (negative) { + exp = accum + } else if (accum != -2147483648) { + exp = -accum + } else throw UnsafeNumber + } else if (consume && current != -1) throw UnsafeNumber val scale = if (dot < 1) exp else exp - dot diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index aac714d8e..37be1314d 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -18,49 +18,72 @@ object DecoderSpec extends ZIOSpecDefault { suite("fromJson")( test("byte") { assert("-123".fromJson[Byte])(isRight(equalTo(-123: Byte))) && + assert("123".fromJson[Byte])(isRight(equalTo(123: Byte))) && assert("\"-123\"".fromJson[Byte])(isRight(equalTo(-123: Byte))) && + assert("\"123\"".fromJson[Byte])(isRight(equalTo(123: Byte))) && + assertTrue("+123".fromJson[Byte].isLeft) && assertTrue("\"Infinity\"".fromJson[Byte].isLeft) && + assertTrue("\"+Infinity\"".fromJson[Byte].isLeft) && assertTrue("\"-Infinity\"".fromJson[Byte].isLeft) && assertTrue("\"NaN\"".fromJson[Byte].isLeft) }, test("short") { assert("-12345".fromJson[Short])(isRight(equalTo(-12345: Short))) && + assert("12345".fromJson[Short])(isRight(equalTo(12345: Short))) && assert("\"-12345\"".fromJson[Short])(isRight(equalTo(-12345: Short))) && + assert("\"12345\"".fromJson[Short])(isRight(equalTo(12345: Short))) && + assertTrue("+12345".fromJson[Short].isLeft) && assertTrue("\"Infinity\"".fromJson[Short].isLeft) && + assertTrue("\"+Infinity\"".fromJson[Short].isLeft) && assertTrue("\"-Infinity\"".fromJson[Short].isLeft) && assertTrue("\"NaN\"".fromJson[Short].isLeft) }, test("int") { assert("-1234567890".fromJson[Int])(isRight(equalTo(-1234567890))) && + assert("1234567890".fromJson[Int])(isRight(equalTo(1234567890))) && assert("\"-1234567890\"".fromJson[Int])(isRight(equalTo(-1234567890))) && + assert("\"1234567890\"".fromJson[Int])(isRight(equalTo(1234567890))) && + assertTrue("+1234567890".fromJson[Int].isLeft) && assertTrue("\"Infinity\"".fromJson[Int].isLeft) && + assertTrue("\"+Infinity\"".fromJson[Int].isLeft) && assertTrue("\"-Infinity\"".fromJson[Int].isLeft) && assertTrue("\"NaN\"".fromJson[Int].isLeft) }, test("long") { assert("-123456789012345678".fromJson[Long])(isRight(equalTo(-123456789012345678L))) && + assert("123456789012345678".fromJson[Long])(isRight(equalTo(123456789012345678L))) && assert("\"-123456789012345678\"".fromJson[Long])(isRight(equalTo(-123456789012345678L))) && + assert("\"123456789012345678\"".fromJson[Long])(isRight(equalTo(123456789012345678L))) && + assertTrue("+123456789012345678".fromJson[Long].isLeft) && assertTrue("\"Infinity\"".fromJson[Long].isLeft) && + assertTrue("\"+Infinity\"".fromJson[Long].isLeft) && assertTrue("\"-Infinity\"".fromJson[Long].isLeft) && assertTrue("\"NaN\"".fromJson[Long].isLeft) }, test("float") { assert("-1.234567e9".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && + assert("1.234567e9".fromJson[Float])(isRight(equalTo(1.234567e9f))) && assert("\"-1.234567e9\"".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && assert("\"Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && + assert("\"+Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && assert("\"-Infinity\"".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && - assertTrue("\"NaN\"".fromJson[Float].isRight) + assertTrue("\"NaN\"".fromJson[Float].isRight) && + assertTrue("+1.234567e9".fromJson[Float].isLeft) }, test("double") { assert("-1.23456789012345e9".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && assert("\"-1.23456789012345e9\"".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && assert("\"Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && + assert("\"+Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && assert("\"-Infinity\"".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && - assertTrue("\"NaN\"".fromJson[Double].isRight) + assertTrue("\"NaN\"".fromJson[Double].isRight) && + assertTrue("+1.23456789012345e9".fromJson[Double].isLeft) }, test("BigDecimal") { + assert("-123.0e123".fromJson[BigDecimal])(isRight(equalTo(BigDecimal("-123.0e123")))) && assert("123.0e123".fromJson[BigDecimal])(isRight(equalTo(BigDecimal("123.0e123")))) && assertTrue("\"Infinity\"".fromJson[BigDecimal].isLeft) && + assertTrue("\"+Infinity\"".fromJson[BigDecimal].isLeft) && assertTrue("\"-Infinity\"".fromJson[BigDecimal].isLeft) && assertTrue("\"NaN\"".fromJson[BigDecimal].isLeft) }, @@ -68,7 +91,11 @@ object DecoderSpec extends ZIOSpecDefault { assert("170141183460469231731687303715884105728".fromJson[BigInteger])( isRight(equalTo(new BigInteger("170141183460469231731687303715884105728"))) ) && + assert("-170141183460469231731687303715884105728".fromJson[BigInteger])( + isRight(equalTo(new BigInteger("-170141183460469231731687303715884105728"))) + ) && assertTrue("\"Infinity\"".fromJson[BigInteger].isLeft) && + assertTrue("\"+Infinity\"".fromJson[BigInteger].isLeft) && assertTrue("\"-Infinity\"".fromJson[BigInteger].isLeft) && assertTrue("\"NaN\"".fromJson[BigInteger].isLeft) }, diff --git a/zio-json/shared/src/test/scala/zio/json/Gens.scala b/zio-json/shared/src/test/scala/zio/json/Gens.scala index 90e8e0f36..e9d5c4ea3 100644 --- a/zio-json/shared/src/test/scala/zio/json/Gens.scala +++ b/zio-json/shared/src/test/scala/zio/json/Gens.scala @@ -17,7 +17,7 @@ object Gens { Gen .bigDecimal((BigDecimal(2).pow(128) - 1) * -1, BigDecimal(2).pow(128) - 1) .map(_.bigDecimal) - .filter(_.toBigInteger.bitLength < 128) + .filter(_.unscaledValue.bitLength < 128) val genUsAsciiString = Gen.string(Gen.oneOf(Gen.char('!', '~'))) From 00b0a2ae18bee04a44b2712e5047d0b36cfb68a8 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 27 Jan 2025 19:14:42 +0100 Subject: [PATCH 113/311] More efficient writing of floats and doubles (#1258) * More efficient writing of floats and doubles * Yet more efficient writing of floats and doubles * Formatting * Tuning writing of floats and doubles for Scala.js --- .../scala/zio/json/internal/SafeNumbers.scala | 600 ++++++++++++++++++ .../scala/zio/json/internal/SafeNumbers.scala | 571 +++++++++++++++++ .../scala/zio/json/internal/SafeNumbers.scala | 571 +++++++++++++++++ .../scala/zio/json/internal/numbers.scala | 551 ---------------- 4 files changed, 1742 insertions(+), 551 deletions(-) create mode 100644 zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala create mode 100644 zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala create mode 100644 zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala new file mode 100644 index 000000000..84d72280b --- /dev/null +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -0,0 +1,600 @@ +/* + * Copyright 2019-2022 John A. De Goes and the ZIO Contributors + * + * 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 zio.json.internal + +/** + * Total, fast, number parsing. + * + * The Java and Scala standard libraries throw exceptions when we attempt to parse an invalid number. Unfortunately, + * exceptions are very expensive, and untrusted data can be maliciously constructed to DOS a server. + * + * This suite of functions mitigates against such attacks by building up the numbers one character at a time, which has + * been shown through extensive benchmarking to be orders of magnitude faster than exception-throwing stdlib parsers, + * for valid and invalid inputs. This approach, proposed by alexknvl, was also benchmarked against regexp-based + * pre-validation. + * + * Note that although the behaviour is identical to the Java stdlib when given the canonical form of a primitive (i.e. + * the .toString) of a number there may be differences in behaviour for non-canonical forms. e.g. the Java stdlib may + * reject "1.0" when parsed as an `BigInteger` but we may parse it as a `1`, although "1.1" would be rejected. Parsing + * of `BigDecimal` preserves the trailing zeros on the right but not on the left, e.g. "000.00001000" will be + * "1.000e-5", which is useful in cases where the trailing zeros denote measurement accuracy. + * + * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit limit on the size of the significand, to + * avoid OOM style attacks, which is 128 bits by default. + * + * Results are contained in a specialisation of Option that avoids boxing. + */ +object SafeNumbers { + import UnsafeNumbers.UnsafeNumber + + def byte(num: String): ByteOption = + try ByteSome(UnsafeNumbers.byte(num)) + catch { case UnsafeNumber => ByteNone } + + def short(num: String): ShortOption = + try ShortSome(UnsafeNumbers.short(num)) + catch { case UnsafeNumber => ShortNone } + + def int(num: String): IntOption = + try IntSome(UnsafeNumbers.int(num)) + catch { case UnsafeNumber => IntNone } + + def long(num: String): LongOption = + try LongSome(UnsafeNumbers.long(num)) + catch { case UnsafeNumber => LongNone } + + def bigInteger( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigInteger] = + try Some(UnsafeNumbers.bigInteger(num, max_bits)) + catch { case UnsafeNumber => None } + + def float(num: String, max_bits: Int = 128): FloatOption = + try FloatSome(UnsafeNumbers.float(num, max_bits)) + catch { case UnsafeNumber => FloatNone } + + def double(num: String, max_bits: Int = 128): DoubleOption = + try DoubleSome(UnsafeNumbers.double(num, max_bits)) + catch { case UnsafeNumber => DoubleNone } + + def bigDecimal( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigDecimal] = + try Some(UnsafeNumbers.bigDecimal(num, max_bits)) + catch { case UnsafeNumber => None } + + // Based on the amazing work of Raffaello Giulietti + // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view + // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java + def toString(x: Double): String = { + val bits = java.lang.Double.doubleToLongBits(x) + val ieeeExponent = (bits >> 52).toInt & 0x7ff + val ieeeMantissa = bits & 0xfffffffffffffL + if (ieeeExponent == 2047) { + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + } else { + val s = new java.lang.StringBuilder(24) + if (bits < 0) s.append('-') + if (x == 0.0f) s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 1075 + var m = ieeeMantissa | 0x10000000000000L + var dv = 0L + var exp = 0 + if (e == 0) dv = m + else if (e >= -52 && e < 0 && m << e == 0) dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if (ieeeExponent == 0) { + e = -1074 + m = ieeeMantissa + if (ieeeMantissa < 3) { + m = (m << 3) + (m << 1) + expShift = 1 + } + } else if (ieeeMantissa == 0 && ieeeExponent > 1) { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val i = exp + 324 << 1 + val g1 = gs(i) + val g0 = gs(i + 1) + val h = (-exp * 108853 >> 15) + e + 2 + val cb = m << 2 + val outm1 = (m.toInt & 0x1) - 1 + val vb = rop(g1, g0, cb << h) + val vbls = rop(g1, g0, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, g0, cb + 2 << h) + val s = vb >> 2 + if ( + s < 100 || { + dv = s / 10 + val sp40 = (dv << 5) + (dv << 3) + val upin = (vbls - sp40).toInt + (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + ) { + val s4 = s << 2 + val uin = (vbls - s4).toInt + dv = (~ { + if ((((s4 + vbrd).toInt + 4) ^ uin) < 0) uin + else (vb.toInt & 0x3) + (s.toInt & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv) + exp += len - 1 + if (exp < -3 || exp >= 7) { + val dotOff = s.length + 1 + val sdv = stripTrailingZeros(dv) + s.append(sdv) + if (sdv < 10) s.append('0') + s.insert(dotOff, '.').append('E').append(exp) + } else if (exp < 0) { + s.append('0').append('.') + while ({ + exp += 1 + exp != 0 + }) s.append('0') + s.append(stripTrailingZeros(dv)) + } else if (exp + 1 < len) { + val dotOff = s.length + exp + 1 + s.append(stripTrailingZeros(dv)) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + s.toString + } + } + + def toString(x: Float): String = { + val bits = java.lang.Float.floatToIntBits(x) + val ieeeExponent = (bits >> 23) & 0xff + val ieeeMantissa = bits & 0x7fffff + if (ieeeExponent == 255) { + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + } else { + val s = new java.lang.StringBuilder(16) + if (bits < 0) s.append('-') + if (x == 0.0f) s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 150 + var m = ieeeMantissa | 0x800000 + var dv, exp = 0 + if (e == 0) dv = m + else if (e >= -23 && e < 0 && m << e == 0) dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if (ieeeExponent == 0) { + e = -149 + m = ieeeMantissa + if (ieeeMantissa < 8) { + m *= 10 + expShift = 1 + } + } else if (ieeeMantissa == 0 && ieeeExponent > 1) { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val g1 = gs(exp + 324 << 1) + 1 + val h = (-exp * 108853 >> 15) + e + 1 + val cb = m << 2 + val outm1 = (m & 0x1) - 1 + val vb = rop(g1, cb << h) + val vbls = rop(g1, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, cb + 2 << h) + val s = vb >> 2 + if ( + s < 100 || { + dv = s / 10 + val sp40 = dv * 40 + val upin = vbls - sp40 + ((sp40 + vbrd + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + ) { + val s4 = s << 2 + val uin = vbls - s4 + dv = (~ { + if (((s4 + vbrd + 4) ^ uin) < 0) uin + else (vb & 0x3) + (s & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv) + exp += len - 1 + if (exp < -3 || exp >= 7) { + val dotOff = s.length + 1 + val sdv = stripTrailingZeros(dv) + s.append(sdv) + if (sdv < 10) s.append('0') + s.insert(dotOff, '.').append('E').append(exp) + } else if (exp < 0) { + s.append('0').append('.') + while ({ + exp += 1 + exp != 0 + }) s.append('0') + s.append(stripTrailingZeros(dv)) + } else if (exp + 1 < len) { + val dotOff = s.length + exp + 1 + s.append(stripTrailingZeros(dv)) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + s.toString + } + } + + @inline + private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { + val x = multiplyHigh(g0, cp) + (g1 * cp >>> 1) + var y = multiplyHigh(g1, cp) + if (x < 0) y += 1 + if (-x != x) y |= 1 + y + } + + @inline + private[this] def rop(g: Long, cp: Int): Int = { + val x = ((g & 0xffffffffL) * cp >>> 32) + (g >>> 32) * cp + (x >>> 31).toInt | -x.toInt >>> 31 + } + + @inline + private[this] def multiplyHigh(x: Long, y: Long): Long = { + val x2 = x & 0xffffffffL + val y2 = y & 0xffffffffL + val b = x2 * y2 + val x1 = x >>> 32 + val y1 = y >>> 32 + val a = x1 * y1 + (((b >>> 32) + (x1 + x2) * (y1 + y2) - b - a) >>> 32) + a + } + + @inline + private[this] def stripTrailingZeros(x: Long): Long = { + var q0 = x.toInt + if ( + q0 == x || { + q0 = ((x >>> 8) * 2.56e-6).toInt // divide a medium positive long by 100000000 + q0 * 100000000L == x + } + ) return stripTrailingZeros(q0).toLong + var y = x + var q1, r1 = 0L + while ({ + q1 = y / 100 + r1 = y - ((q1 << 6) + (q1 << 5) + (q1 << 2)) + r1 == 0 + }) y = q1 + q1 = y / 10 + r1 = y - ((q1 << 3) + (q1 << 1)) + if (r1 == 0) return q1 + y + } + + private[this] def stripTrailingZeros(x: Int): Int = { + var q0 = x + var q1 = 0 + while ({ + q1 = q0 / 100 + q1 * 100 == q0 // check if q is divisible by 100 + }) q0 = q1 + q1 = q0 / 10 + if (q1 * 10 == q0) return q1 // check if q is divisible by 10 + q0 + } + + @inline + private[this] def digitCount(x: Long): Int = + if (x >= 1000000000000000L) { + if (x >= 10000000000000000L) 17 + else 16 + } else if (x >= 10000000000000L) { + if (x >= 100000000000000L) 15 + else 14 + } else if (x >= 100000000000L) { + if (x >= 1000000000000L) 13 + else 12 + } else if (x >= 1000000000L) { + if (x >= 10000000000L) 11 + else 10 + } else digitCount(x.toInt) + + private[this] def digitCount(x: Int): Int = + if (x < 100) { + if (x < 10) 1 + else 2 + } else if (x < 10000) { + if (x < 1000) 3 + else 4 + } else if (x < 1000000) { + if (x < 100000) 5 + else 6 + } else if (x < 100000000) { + if (x < 10000000) 7 + else 8 + } else { + if (x < 1000000000) 9 + else 10 + } + + private[this] val gs: Array[Long] = Array( + 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, + 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, + 7466108948025751031L, 8492109508320019073L, 5972887158420600825L, 4949013199285060097L, 4778309726736480660L, + 3959210559428048077L, 7645295562778369056L, 6334736895084876923L, 6116236450222695245L, 3223115108696946377L, + 4892989160178156196L, 2578492086957557102L, 7828782656285049914L, 436238524390181040L, 6263026125028039931L, + 2193665226883099993L, 5010420900022431944L, 9133629810990300641L, 8016673440035891111L, 9079784475471615541L, + 6413338752028712889L, 5419153173006337271L, 5130671001622970311L, 6179996945776024979L, 8209073602596752498L, + 6198646298499729642L, 6567258882077401998L, 8648265853541694037L, 5253807105661921599L, 1384589460720489745L, + 8406091369059074558L, 5904691951894693915L, 6724873095247259646L, 8413102376257665455L, 5379898476197807717L, + 4885807493635177203L, 8607837561916492348L, 438594360332462878L, 6886270049533193878L, 4040224303007880625L, + 5509016039626555102L, 6921528257148214824L, 8814425663402488164L, 3695747581953323071L, 7051540530721990531L, + 4801272472933613619L, 5641232424577592425L, 1996343570975935733L, 9025971879324147880L, 3194149713561497173L, + 7220777503459318304L, 2555319770849197738L, 5776622002767454643L, 3888930224050313352L, 4621297602213963714L, + 6800492993982161005L, 7394076163542341943L, 5346765568258592123L, 5915260930833873554L, 7966761269348784022L, + 4732208744667098843L, 8218083422849982379L, 7571533991467358150L, 2080887032334240837L, 6057227193173886520L, + 1664709625867392670L, 4845781754539109216L, 1331767700693914136L, 7753250807262574745L, 7664851543223128102L, + 6202600645810059796L, 6131881234578502482L, 4962080516648047837L, 3060830580291846824L, 7939328826636876539L, + 6742003335837910079L, 6351463061309501231L, 7238277076041283225L, 5081170449047600985L, 3945947253462071419L, + 8129872718476161576L, 6313515605539314269L, 6503898174780929261L, 3206138077060496254L, 5203118539824743409L, + 720236054277441842L, 8324989663719589454L, 4841726501585817270L, 6659991730975671563L, 5718055608639608977L, + 5327993384780537250L, 8263793301653597505L, 8524789415648859601L, 3998697245790980200L, 6819831532519087681L, + 1354283389261828999L, 5455865226015270144L, 8462124340893283845L, 8729384361624432231L, 8005375723316388668L, + 6983507489299545785L, 4559626171282155773L, 5586805991439636628L, 3647700937025724618L, 8938889586303418605L, + 3991647091870204227L, 7151111669042734884L, 3193317673496163382L, 5720889335234187907L, 4399328546167885867L, + 9153422936374700651L, 8883600081239572549L, 7322738349099760521L, 5262205657620702877L, 5858190679279808417L, + 2365090118725607140L, 4686552543423846733L, 7426095317093351197L, 7498484069478154774L, 813706063123630946L, + 5998787255582523819L, 2495639257869859918L, 4799029804466019055L, 3841185813666843096L, 7678447687145630488L, + 6145897301866948954L, 6142758149716504390L, 8606066656235469486L, 4914206519773203512L, 6884853324988375589L, + 7862730431637125620L, 3637067690497580296L, 6290184345309700496L, 2909654152398064237L, 5032147476247760397L, + 483048914547496228L, 8051435961996416635L, 2617552670646949126L, 6441148769597133308L, 2094042136517559301L, + 5152919015677706646L, 5364582523955957764L, 8244670425084330634L, 4893983223587622099L, 6595736340067464507L, + 5759860986241052841L, 5276589072053971606L, 918539974250931950L, 8442542515286354569L, 7003687180914356604L, + 6754034012229083655L, 7447624152102440445L, 5403227209783266924L, 5958099321681952356L, 8645163535653227079L, + 3998935692578258285L, 6916130828522581663L, 5043822961433561789L, 5532904662818065330L, 7724407183888759755L, + 8852647460508904529L, 3135679457367239799L, 7082117968407123623L, 4353217973264747001L, 5665694374725698898L, + 7171923193353707924L, 9065110999561118238L, 407030665140201709L, 7252088799648894590L, 4014973346854071690L, + 5801671039719115672L, 3211978677483257352L, 4641336831775292537L, 8103606164099471367L, 7426138930840468060L, + 5587072233075333540L, 5940911144672374448L, 4469657786460266832L, 4752728915737899558L, 7265075043910123789L, + 7604366265180639294L, 556073626030467093L, 6083493012144511435L, 2289533308195328836L, 4866794409715609148L, + 1831626646556263069L, 7786871055544974637L, 1085928227119065748L, 6229496844435979709L, 6402765803808118083L, + 4983597475548783767L, 6966887050417449628L, 7973755960878054028L, 3768321651184098759L, 6379004768702443222L, + 6704006135689189330L, 5103203814961954578L, 1673856093809441141L, 8165126103939127325L, 833495342724150664L, + 6532100883151301860L, 666796274179320531L, 5225680706521041488L, 533437019343456425L, 8361089130433666380L, + 8232196860433350926L, 6688871304346933104L, 6585757488346680741L, 5351097043477546483L, 7113280398048299755L, + 8561755269564074374L, 313202192651548637L, 6849404215651259499L, 2095236161492194072L, 5479523372521007599L, + 3520863336564710419L, 8767237396033612159L, 99358116390671185L, 7013789916826889727L, 1924160900483492110L, + 5611031933461511781L, 7073351942499659173L, 8977651093538418850L, 7628014293257544353L, 7182120874830735080L, + 6102411434606035483L, 5745696699864588064L, 4881929147684828386L, 9193114719783340903L, 2277063414182859933L, + 7354491775826672722L, 5510999546088198270L, 5883593420661338178L, 719450822128648293L, 4706874736529070542L, + 4264909472444828957L, 7530999578446512867L, 8668529563282681493L, 6024799662757210294L, 3245474835884234871L, + 4819839730205768235L, 4441054276078343059L, 7711743568329229176L, 7105686841725348894L, 6169394854663383341L, + 3839875066009323953L, 4935515883730706673L, 1227225645436504001L, 7896825413969130677L, 118886625327451240L, + 6317460331175304541L, 5629132522374826477L, 5053968264940243633L, 2658631610528906020L, 8086349223904389813L, + 2409136169475294470L, 6469079379123511850L, 5616657750322145900L, 5175263503298809480L, 4493326200257716720L, + 8280421605278095168L, 7189321920412346751L, 6624337284222476135L, 217434314217011916L, 5299469827377980908L, + 173947451373609533L, 8479151723804769452L, 7657013551681595899L, 6783321379043815562L, 2436262026603366396L, + 5426657103235052449L, 7483032843395558602L, 8682651365176083919L, 6438829327320028278L, 6946121092140867135L, + 6995737869226977784L, 5556896873712693708L, 5596590295381582227L, 8891034997940309933L, 7109870065239576402L, + 7112827998352247947L, 153872830078795637L, 5690262398681798357L, 5657121486175901994L, 9104419837890877372L, + 1672696748397622544L, 7283535870312701897L, 6872180620830963520L, 5826828696250161518L, 1808395681922860493L, + 4661462957000129214L, 5136065360280198718L, 7458340731200206743L, 2683681354335452463L, 5966672584960165394L, + 5836293898210272294L, 4773338067968132315L, 6513709525939172997L, 7637340908749011705L, 1198563204647900987L, + 6109872726999209364L, 958850563718320789L, 4887898181599367491L, 2611754858345611793L, 7820637090558987986L, + 489458958611068546L, 6256509672447190388L, 7770264796372675483L, 5005207737957752311L, 682188614985274902L, + 8008332380732403697L, 6625525006089305327L, 6406665904585922958L, 1611071190129533939L, 5125332723668738366L, + 4978205766845537474L, 8200532357869981386L, 4275780412210949635L, 6560425886295985109L, 1575949922397804547L, + 5248340709036788087L, 3105434345289198799L, 8397345134458860939L, 6813369359833673240L, 6717876107567088751L, + 7295369895237893754L, 5374300886053671001L, 3991621508819359841L, 8598881417685873602L, 2697245599369065423L, + 6879105134148698881L, 7691819701608117823L, 5503284107318959105L, 4308781353915539097L, 8805254571710334568L, + 6894050166264862555L, 7044203657368267654L, 9204588947753800367L, 5635362925894614123L, 9208345565573995455L, + 9016580681431382598L, 3665306460692661759L, 7213264545145106078L, 6621593983296039730L, 5770611636116084862L, + 8986624001378742108L, 4616489308892867890L, 3499950386361083363L, 7386382894228588624L, 5599920618177733380L, + 5909106315382870899L, 6324610901913141866L, 4727285052306296719L, 6904363128901468655L, 7563656083690074751L, + 5512957784129484362L, 6050924866952059801L, 2565691819932632328L, 4840739893561647841L, 207879048575150701L, + 7745183829698636545L, 5866629699833106606L, 6196147063758909236L, 4693303759866485285L, 4956917651007127389L, + 1909968600522233067L, 7931068241611403822L, 6745298575577483229L, 6344854593289123058L, 1706890045720076260L, + 5075883674631298446L, 5054860851317971332L, 8121413879410077514L, 4398428547366843807L, 6497131103528062011L, + 5363417245264430207L, 5197704882822449609L, 2446059388840589004L, 8316327812515919374L, 7603043836886852730L, + 6653062250012735499L, 7927109476880437346L, 5322449800010188399L, 8186361988875305038L, 8515919680016301439L, + 7564155960087622576L, 6812735744013041151L, 7895999175441053223L, 5450188595210432921L, 4472124932981887417L, + 8720301752336692674L, 3466051078029109543L, 6976241401869354139L, 4617515269794242796L, 5580993121495483311L, + 5538686623206349399L, 8929588994392773298L, 5172549782388248714L, 7143671195514218638L, 7827388640652509295L, + 5714936956411374911L, 727887690409141951L, 9143899130258199857L, 6698643526767492606L, 7315119304206559886L, + 1669566006672083762L, 5852095443365247908L, 8714350434821487656L, 4681676354692198327L, 1437457125744324640L, + 7490682167507517323L, 4144605808561874585L, 5992545734006013858L, 7005033461591409992L, 4794036587204811087L, + 70003547160262509L, 7670458539527697739L, 1956680082827375175L, 6136366831622158191L, 3410018473632855302L, + 4909093465297726553L, 883340371535329080L, 7854549544476362484L, 8792042223940347174L, 6283639635581089987L, + 8878308186523232901L, 5026911708464871990L, 3413297734476675998L, 8043058733543795184L, 5461276375162681596L, + 6434446986835036147L, 6213695507501100438L, 5147557589468028918L, 1281607591258970028L, 8236092143148846269L, + 205897738643396882L, 6588873714519077015L, 2009392598285672668L, 5271098971615261612L, 1607514078628538134L, + 8433758354584418579L, 4416696933176616176L, 6747006683667534863L, 5378031953912248102L, 5397605346934027890L, + 7991774377871708805L, 8636168555094444625L, 3563466967739958280L, 6908934844075555700L, 2850773574191966624L, + 5527147875260444560L, 2280618859353573299L, 8843436600416711296L, 3648990174965717279L, 7074749280333369037L, + 1074517732601618662L, 5659799424266695229L, 6393637408194160414L, 9055679078826712367L, 4695796630997791177L, + 7244543263061369894L, 67288490056322619L, 5795634610449095915L, 1898505199416013257L, 4636507688359276732L, + 1518804159532810606L, 7418412301374842771L, 4274761062623452130L, 5934729841099874217L, 1575134442727806543L, + 4747783872879899373L, 6794130776295110719L, 7596454196607838997L, 9025934834701221989L, 6077163357286271198L, + 3531399053019067268L, 4861730685829016958L, 6514468057157164137L, 7778769097326427133L, 8578474484080507458L, + 6223015277861141707L, 1328756365151540482L, 4978412222288913365L, 6597028314234097870L, 7965459555662261385L, + 1331873265919780784L, 6372367644529809108L, 1065498612735824627L, 5097894115623847286L, 4541747704930570025L, + 8156630584998155658L, 3577447513147001717L, 6525304467998524526L, 6551306825259511697L, 5220243574398819621L, + 3396371052836654196L, 8352389719038111394L, 1744844869796736390L, 6681911775230489115L, 3240550303208344274L, + 5345529420184391292L, 2592440242566675419L, 8552847072295026067L, 5992578795477635832L, 6842277657836020854L, + 1104714221640198342L, 5473822126268816683L, 2728445784683113836L, 8758115402030106693L, 2520838848122026975L, + 7006492321624085354L, 5706019893239531903L, 5605193857299268283L, 6409490321962580684L, 8968310171678829253L, + 8410510107769173933L, 7174648137343063403L, 1194384864102473662L, 5739718509874450722L, 4644856706023889253L, + 9183549615799121156L, 53073100154402158L, 7346839692639296924L, 7421156109607342373L, 5877471754111437539L, + 7781599295056829060L, 4701977403289150031L, 8069953843416418410L, 7523163845262640050L, 9222577334724359132L, + 6018531076210112040L, 7378061867779487306L, 4814824860968089632L, 5902449494223589845L, 7703719777548943412L, + 2065221561273923105L, 6162975822039154729L, 7186200471132003969L, 4930380657631323783L, 7593634784276558337L, + 7888609052210118054L, 1081769210616762369L, 6310887241768094443L, 2710089775864365057L, 5048709793414475554L, + 5857420635433402369L, 8077935669463160887L, 3837849794580578305L, 6462348535570528709L, 8604303057777328129L, + 5169878828456422967L, 8728116853592817665L, 8271806125530276748L, 6586289336264687617L, 6617444900424221398L, + 8958380283753660417L, 5293955920339377119L, 1632681004890062849L, 8470329472543003390L, 6301638422566010881L, + 6776263578034402712L, 5041310738052808705L, 5421010862427522170L, 343699775700336641L, 8673617379884035472L, + 549919641120538625L, 6938893903907228377L, 5973958935009296385L, 5551115123125782702L, 1089818333265526785L, + 8881784197001252323L, 3588383740595798017L, 7105427357601001858L, 6560055807218548737L, 5684341886080801486L, + 8937393460516749313L, 9094947017729282379L, 1387108685230112769L, 7275957614183425903L, 2954361355555045377L, + 5820766091346740722L, 6052837899185946625L, 4656612873077392578L, 1152921504606846977L, 7450580596923828125L, 1L, + 5960464477539062500L, 1L, 4768371582031250000L, 1L, 7629394531250000000L, 1L, 6103515625000000000L, 1L, + 4882812500000000000L, 1L, 7812500000000000000L, 1L, 6250000000000000000L, 1L, 5000000000000000000L, 1L, + 8000000000000000000L, 1L, 6400000000000000000L, 1L, 5120000000000000000L, 1L, 8192000000000000000L, 1L, + 6553600000000000000L, 1L, 5242880000000000000L, 1L, 8388608000000000000L, 1L, 6710886400000000000L, 1L, + 5368709120000000000L, 1L, 8589934592000000000L, 1L, 6871947673600000000L, 1L, 5497558138880000000L, 1L, + 8796093022208000000L, 1L, 7036874417766400000L, 1L, 5629499534213120000L, 1L, 9007199254740992000L, 1L, + 7205759403792793600L, 1L, 5764607523034234880L, 1L, 4611686018427387904L, 1L, 7378697629483820646L, + 3689348814741910324L, 5902958103587056517L, 1106804644422573097L, 4722366482869645213L, 6419466937650923963L, + 7555786372591432341L, 8426472692870523179L, 6044629098073145873L, 4896503746925463381L, 4835703278458516698L, + 7606551812282281028L, 7737125245533626718L, 1102436455425918676L, 6189700196426901374L, 4571297979082645264L, + 4951760157141521099L, 5501712790637071373L, 7922816251426433759L, 3268717242906448711L, 6338253001141147007L, + 4459648201696114131L, 5070602400912917605L, 9101741783469756789L, 8112963841460668169L, 5339414816696835055L, + 6490371073168534535L, 6116206260728423206L, 5192296858534827628L, 4892965008582738565L, 8307674973655724205L, + 5984069606361426541L, 6646139978924579364L, 4787255685089141233L, 5316911983139663491L, 5674478955442268148L, + 8507059173023461586L, 5389817513965718714L, 6805647338418769269L, 2467179603801619810L, 5444517870735015415L, + 3818418090412251009L, 8711228593176024664L, 6109468944659601615L, 6968982874540819731L, 6732249563098636453L, + 5575186299632655785L, 3541125243107954001L, 8920298079412249256L, 5665800388972726402L, 7136238463529799405L, + 2687965903807225960L, 5708990770823839524L, 2150372723045780768L, 9134385233318143238L, 7129945171615159552L, + 7307508186654514591L, 169932915179262157L, 5846006549323611672L, 7514643961627230372L, 4676805239458889338L, + 2322366354559873974L, 7482888383134222941L, 1871111759924843197L, 5986310706507378352L, 8875587037423695204L, + 4789048565205902682L, 3411120815197045840L, 7662477704329444291L, 7302467711686228506L, 6129982163463555433L, + 3997299761978027643L, 4903985730770844346L, 6887188624324332438L, 7846377169233350954L, 7330152984177021577L, + 6277101735386680763L, 7708796794712572423L, 5021681388309344611L, 633014213657192454L, 8034690221294951377L, + 6546845963964373411L, 6427752177035961102L, 1548127956429588405L, 5142201741628768881L, 6772525587256536209L, + 8227522786606030210L, 7146692124868547611L, 6582018229284824168L, 5717353699894838089L, 5265614583427859334L, + 8263231774657780795L, 8424983333484574935L, 7687147617339583786L, 6739986666787659948L, 6149718093871667029L, + 5391989333430127958L, 8609123289839243947L, 8627182933488204734L, 2706550819517059345L, 6901746346790563787L, + 4009915062984602637L, 5521397077432451029L, 8741955272500547595L, 8834235323891921647L, 8453105213888010667L, + 7067388259113537318L, 3073135356368498210L, 5653910607290829854L, 6147857099836708891L, 9046256971665327767L, + 4302548137625868741L, 7237005577332262213L, 8976061732213560478L, 5789604461865809771L, 1646826163657982898L, + 4631683569492647816L, 8696158560410206965L, 7410693711188236507L, 1001132845059645012L, 5928554968950589205L, + 6334929498160581494L, 4742843975160471364L, 5067943598528465196L, 7588550360256754183L, 2574686535532678828L, + 6070840288205403346L, 5749098043168053386L, 4856672230564322677L, 2754604027163487547L, 7770675568902916283L, + 6252040850832535236L, 6216540455122333026L, 8690981495407938512L, 4973232364097866421L, 5108110788955395648L, + 7957171782556586274L, 4483628447586722714L, 6365737426045269019L, 5431577165440333333L, 5092589940836215215L, + 6189936139723221828L, 8148143905337944345L, 680525786702379117L, 6518515124270355476L, 544420629361903293L, + 5214812099416284380L, 7814234132973343281L, 8343699359066055009L, 3279402575902573442L, 6674959487252844007L, + 4468196468093013915L, 5339967589802275205L, 9108580396587276617L, 8543948143683640329L, 5350356597684866779L, + 6835158514946912263L, 6124959685518848585L, 5468126811957529810L, 8589316563156989191L, 8749002899132047697L, + 4519534464196406897L, 6999202319305638157L, 9149650793469991003L, 5599361855444510526L, 3630371820034082479L, + 8958978968711216842L, 2119246097312621643L, 7167183174968973473L, 7229420099962962799L, 5733746539975178779L, + 249512857857504755L, 9173994463960286046L, 4088569387313917931L, 7339195571168228837L, 1426181102480179183L, + 5871356456934583069L, 6674968104097008831L, 4697085165547666455L, 7184648890648562227L, 7515336264876266329L, + 2272066188182923754L, 6012269011901013063L, 3662327357917294165L, 4809815209520810450L, 6619210701075745655L, + 7695704335233296721L, 1367365084866417240L, 6156563468186637376L, 8472589697376954439L, 4925250774549309901L, + 4933397350530608390L, 7880401239278895842L, 4204086946107063100L, 6304320991423116673L, 8897292778998515965L, + 5043456793138493339L, 1583811001085947287L, 8069530869021589342L, 6223446416479425982L, 6455624695217271474L, + 1289408318441630463L, 5164499756173817179L, 2876201062124259532L, 8263199609878107486L, 8291270514140725574L, + 6610559687902485989L, 4788342003941625298L, 5288447750321988791L, 5675348010524255400L, 8461516400515182066L, + 5391208002096898316L, 6769213120412145653L, 2468291994306563491L, 5415370496329716522L, 5663982410187161116L, + 8664592794127546436L, 1683674226815637140L, 6931674235302037148L, 8725637010936330358L, 5545339388241629719L, + 1446486386636198802L, 8872543021186607550L, 6003727033359828406L, 7098034416949286040L, 4802981626687862725L, + 5678427533559428832L, 3842385301350290180L, 9085484053695086131L, 7992490889531419449L, 7268387242956068905L, + 4549318304254180398L, 5814709794364855124L, 3639454643403344318L, 4651767835491884099L, 4756238122093630616L, + 7442828536787014559L, 2075957773236943501L, 5954262829429611647L, 3505440625960509963L, 4763410263543689317L, + 8338375722881273455L, 7621456421669902908L, 5962703527126216881L, 6097165137335922326L, 8459511636442883828L, + 4877732109868737861L, 4922934901783351901L, 7804371375789980578L, 4187347028111452718L, 6243497100631984462L, + 7039226437231072498L, 4994797680505587570L, 1942032335042947675L, 7991676288808940112L, 3107251736068716280L, + 6393341031047152089L, 8019824610967838509L, 5114672824837721671L, 8260534096145225969L, 8183476519740354675L, + 304133702235675419L, 6546781215792283740L, 243306961788540335L, 5237424972633826992L, 194645569430832268L, + 8379879956214123187L, 2156107318460286790L, 6703903964971298549L, 7258909076881094917L, 5363123171977038839L, + 7651801668875831096L, 8580997075163262143L, 6708859448088464268L, 6864797660130609714L, 9056436373212681737L, + 5491838128104487771L, 9089823505941100552L, 8786941004967180435L, 1630996757909074751L, 7029552803973744348L, + 1304797406327259801L, 5623642243178995478L, 4733186739803718164L, 8997827589086392765L, 5728424376314993901L, + 7198262071269114212L, 4582739501051995121L, 5758609657015291369L, 9200214822954461581L, 9213775451224466191L, + 9186320494614273045L, 7371020360979572953L, 5504381988320463275L, 5896816288783658362L, 8092854405398280943L, + 4717453031026926690L, 2784934709576714431L, 7547924849643082704L, 4455895535322743090L, 6038339879714466163L, + 5409390835629149634L, 4830671903771572930L, 8016861483245230030L, 7729075046034516689L, 3603606336337592240L, + 6183260036827613351L, 4727559476441028954L, 4946608029462090681L, 1937373173781868001L, 7914572847139345089L, + 8633820300163854287L, 6331658277711476071L, 8751730647502038591L, 5065326622169180857L, 5156710110630675711L, + 8104522595470689372L, 872038547525260492L, 6483618076376551497L, 6231654060133073878L, 5186894461101241198L, + 1295974433364548779L, 8299031137761985917L, 228884686012322885L, 6639224910209588733L, 5717130970922723793L, + 5311379928167670986L, 8263053591480089358L, 8498207885068273579L, 308164894771456841L, 6798566308054618863L, + 2091206323188120634L, 5438853046443695090L, 5362313873292406831L, 8702164874309912144L, 8579702197267850929L, + 6961731899447929715L, 8708436165185235905L, 5569385519558343772L, 6966748932148188724L, 8911016831293350036L, + 3768100661953281312L, 7128813465034680029L, 1169806122191669888L, 5703050772027744023L, 2780519305124291072L, + 9124881235244390437L, 2604156480827910553L, 7299904988195512349L, 7617348406775193928L, 5839923990556409879L, + 7938553132791110304L, 4671939192445127903L, 8195516913603843405L, 7475102707912204646L, 2044780617540418478L, + 5980082166329763716L, 9014522123516155429L, 4784065733063810973L, 5366943291441969181L, 7654505172902097557L, + 6742434858936195528L, 6123604138321678046L, 1704599072407046100L, 4898883310657342436L, 8742376887409457526L, + 7838213297051747899L, 1075082168258445910L, 6270570637641398319L, 2704740141977711890L, 5016456510113118655L, + 4008466520953124674L, 8026330416180989848L, 6413546433524999478L, 6421064332944791878L, 8820185961561909905L, + 5136851466355833503L, 1522125547136662440L, 8218962346169333605L, 590726468047704741L, 6575169876935466884L, + 472581174438163793L, 5260135901548373507L, 2222739346921486196L, 8416217442477397611L, 5401057362445333075L, + 6732973953981918089L, 2476171482585311299L, 5386379163185534471L, 3825611593439204201L, 8618206661096855154L, + 2431629734760816398L, 6894565328877484123L, 3789978195179608280L, 5515652263101987298L, 6721331370885596947L, + 8825043620963179677L, 8909455786045999954L, 7060034896770543742L, 3438215814094889640L, 5648027917416434993L, + 8284595873388777197L, 9036844667866295990L, 2187306953196312545L, 7229475734293036792L, 1749845562557050036L, + 5783580587434429433L, 6933899672158505514L, 4626864469947543547L, 13096515613938926L, 7402983151916069675L, + 1865628832353257443L, 5922386521532855740L, 1492503065882605955L, 4737909217226284592L, 1194002452706084764L, + 7580654747562055347L, 3755078331700690783L, 6064523798049644277L, 8538085887473418112L, 4851619038439715422L, + 3141119895236824166L, 7762590461503544675L, 6870466239749873827L, 6210072369202835740L, 5496372991799899062L, + 4968057895362268592L, 4397098393439919250L, 7948892632579629747L, 8880031836874825961L, 6359114106063703798L, + 3414676654757950445L, 5087291284850963038L, 6421090138548270680L, 8139666055761540861L, 8429069814306277926L, + 6511732844609232689L, 4898581444074067179L, 5209386275687386151L, 5763539562630208905L, 8335018041099817842L, + 5532314485466423924L, 6668014432879854274L, 736502773631228816L, 5334411546303883419L, 2433876626275938215L, + 8535058474086213470L, 7583551416783411467L, 6828046779268970776L, 6066841133426729173L, 5462437423415176621L, + 3008798499370428177L, 8739899877464282594L, 1124728784250774760L, 6991919901971426075L, 2744457434771574970L, + 5593535921577140860L, 2195565947817259976L, 8949657474523425376L, 3512905516507615961L, 7159725979618740301L, + 965650005835137607L, 5727780783694992240L, 8151217634151930732L, 9164449253911987585L, 3818576177788313364L, + 7331559403129590068L, 3054860942230650691L, 5865247522503672054L, 6133237568526430876L, 4692198018002937643L, + 6751264462192099863L, 7507516828804700229L, 8957348732136404618L, 6006013463043760183L, 9010553393080078856L, + 4804810770435008147L, 1674419492351197600L, 7687697232696013035L, 4523745595132871322L, 6150157786156810428L, + 3618996476106297057L, 4920126228925448342L, 6584545995626947969L, 7872201966280717348L, 3156575963519296104L, + 6297761573024573878L, 6214609585557347207L, 5038209258419659102L, 8661036483187788089L, 8061134813471454564L, + 6478960743616640295L, 6448907850777163651L, 7027843002264267398L, 5159126280621730921L, 3777599994440458757L, + 8254602048994769474L, 2354811176362823687L, 6603681639195815579L, 3728523348461214111L, 5282945311356652463L, + 4827493086139926451L, 8452712498170643941L, 5879314530452927160L, 6762169998536515153L, 2858777216991386566L, + 5409735998829212122L, 5976370588335019576L, 8655577598126739396L, 2183495311852210675L, 6924462078501391516L, + 9125493878965589187L, 5539569662801113213L, 5455720695801516188L, 8863311460481781141L, 6884478705911470739L, + 7090649168385424913L, 3662908557358221429L, 5672519334708339930L, 6619675660628487467L, 9076030935533343889L, + 1368109020150804139L, 7260824748426675111L, 2939161623491598473L, 5808659798741340089L, 506654891422323617L, + 4646927838993072071L, 2249998320508814055L, 7435084542388915313L, 9134020534926967972L, 5948067633911132251L, + 1773193205828708893L, 4758454107128905800L, 8797252194146787761L, 7613526571406249281L, 4852231473780084609L, + 6090821257124999425L, 2037110771653112526L, 4872657005699999540L, 1629688617322490021L, 7796251209119999264L, + 2607501787715984033L, 6237000967295999411L, 3930675837543742388L, 4989600773836799529L, 1299866262664038749L, + 7983361238138879246L, 5769134835004372321L, 6386688990511103397L, 2770633460632542696L, 5109351192408882717L, + 7750529990618899641L, 8174961907854212348L, 5022150355506418780L, 6539969526283369878L, 7707069099147045347L, + 5231975621026695903L, 631632057204770793L, 8371160993642713444L, 8389308921011453915L, 6696928794914170755L, + 8556121544180118293L, 5357543035931336604L, 6844897235344094635L, 8572068857490138567L, 5417812354437685931L, + 6857655085992110854L, 644901068808238421L, 5486124068793688683L, 2360595262417545899L, 8777798510069901893L, + 1932278012497118276L, 7022238808055921514L, 5235171224739604944L, 5617791046444737211L, 6032811387162639117L, + 8988465674311579538L, 5963149404718312264L, 7190772539449263630L, 8459868338516560134L, 5752618031559410904L, + 6767894670813248108L, 9204188850495057447L, 5294608251188331487L + ) +} diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala new file mode 100644 index 000000000..0bb7ba4af --- /dev/null +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -0,0 +1,571 @@ +/* + * Copyright 2019-2022 John A. De Goes and the ZIO Contributors + * + * 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 zio.json.internal + +/** + * Total, fast, number parsing. + * + * The Java and Scala standard libraries throw exceptions when we attempt to parse an invalid number. Unfortunately, + * exceptions are very expensive, and untrusted data can be maliciously constructed to DOS a server. + * + * This suite of functions mitigates against such attacks by building up the numbers one character at a time, which has + * been shown through extensive benchmarking to be orders of magnitude faster than exception-throwing stdlib parsers, + * for valid and invalid inputs. This approach, proposed by alexknvl, was also benchmarked against regexp-based + * pre-validation. + * + * Note that although the behaviour is identical to the Java stdlib when given the canonical form of a primitive (i.e. + * the .toString) of a number there may be differences in behaviour for non-canonical forms. e.g. the Java stdlib may + * reject "1.0" when parsed as an `BigInteger` but we may parse it as a `1`, although "1.1" would be rejected. Parsing + * of `BigDecimal` preserves the trailing zeros on the right but not on the left, e.g. "000.00001000" will be + * "1.000e-5", which is useful in cases where the trailing zeros denote measurement accuracy. + * + * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit limit on the size of the significand, to + * avoid OOM style attacks, which is 128 bits by default. + * + * Results are contained in a specialisation of Option that avoids boxing. + */ +object SafeNumbers { + import UnsafeNumbers.UnsafeNumber + + def byte(num: String): ByteOption = + try ByteSome(UnsafeNumbers.byte(num)) + catch { case UnsafeNumber => ByteNone } + + def short(num: String): ShortOption = + try ShortSome(UnsafeNumbers.short(num)) + catch { case UnsafeNumber => ShortNone } + + def int(num: String): IntOption = + try IntSome(UnsafeNumbers.int(num)) + catch { case UnsafeNumber => IntNone } + + def long(num: String): LongOption = + try LongSome(UnsafeNumbers.long(num)) + catch { case UnsafeNumber => LongNone } + + def bigInteger( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigInteger] = + try Some(UnsafeNumbers.bigInteger(num, max_bits)) + catch { case UnsafeNumber => None } + + def float(num: String, max_bits: Int = 128): FloatOption = + try FloatSome(UnsafeNumbers.float(num, max_bits)) + catch { case UnsafeNumber => FloatNone } + + def double(num: String, max_bits: Int = 128): DoubleOption = + try DoubleSome(UnsafeNumbers.double(num, max_bits)) + catch { case UnsafeNumber => DoubleNone } + + def bigDecimal( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigDecimal] = + try Some(UnsafeNumbers.bigDecimal(num, max_bits)) + catch { case UnsafeNumber => None } + + // Based on the amazing work of Raffaello Giulietti + // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view + // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java + def toString(x: Double): String = { + val bits = java.lang.Double.doubleToLongBits(x) + val ieeeExponent = (bits >> 52).toInt & 0x7ff + val ieeeMantissa = bits & 0xfffffffffffffL + if (ieeeExponent == 2047) { + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + } else { + val s = new java.lang.StringBuilder(24) + if (bits < 0) s.append('-') + if (x == 0.0f) s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 1075 + var m = ieeeMantissa | 0x10000000000000L + var dv = 0L + var exp = 0 + if (e == 0) dv = m + else if (e >= -52 && e < 0 && m << e == 0) dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if (ieeeExponent == 0) { + e = -1074 + m = ieeeMantissa + if (ieeeMantissa < 3) { + m *= 10 + expShift = 1 + } + } else if (ieeeMantissa == 0 && ieeeExponent > 1) { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val i = exp + 324 << 1 + val g1 = gs(i) + val g0 = gs(i + 1) + val h = (-exp * 108853 >> 15) + e + 2 + val cb = m << 2 + val outm1 = (m.toInt & 0x1) - 1 + val vb = rop(g1, g0, cb << h) + val vbls = rop(g1, g0, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, g0, cb + 2 << h) + val s = vb >> 2 + if ( + s < 100 || { + dv = Math.multiplyHigh(s, 1844674407370955168L) + val sp40 = dv * 40 + val upin = (vbls - sp40).toInt + (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + ) { + val s4 = s << 2 + val uin = (vbls - s4).toInt + dv = (~ { + if ((((s4 + vbrd).toInt + 4) ^ uin) < 0) uin + else (vb.toInt & 0x3) + (s.toInt & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv) + exp += len - 1 + if (exp < -3 || exp >= 7) { + val dotOff = s.length + 1 + val sdv = stripTrailingZeros(dv) + s.append(sdv) + if (sdv < 10) s.append('0') + s.insert(dotOff, '.').append('E').append(exp) + } else if (exp < 0) { + s.append('0').append('.') + while ({ + exp += 1 + exp != 0 + }) s.append('0') + s.append(stripTrailingZeros(dv)) + } else if (exp + 1 < len) { + val dotOff = s.length + exp + 1 + s.append(stripTrailingZeros(dv)) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + s.toString + } + } + + def toString(x: Float): String = { + val bits = java.lang.Float.floatToIntBits(x) + val ieeeExponent = (bits >> 23) & 0xff + val ieeeMantissa = bits & 0x7fffff + if (ieeeExponent == 255) { + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + } else { + val s = new java.lang.StringBuilder(16) + if (bits < 0) s.append('-') + if (x == 0.0f) s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 150 + var m = ieeeMantissa | 0x800000 + var dv, exp = 0 + if (e == 0) dv = m + else if (e >= -23 && e < 0 && m << e == 0) dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if (ieeeExponent == 0) { + e = -149 + m = ieeeMantissa + if (ieeeMantissa < 8) { + m *= 10 + expShift = 1 + } + } else if (ieeeMantissa == 0 && ieeeExponent > 1) { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val g1 = gs(exp + 324 << 1) + 1 + val h = (-exp * 108853 >> 15) + e + 1 + val cb = m << 2 + val outm1 = (m & 0x1) - 1 + val vb = rop(g1, cb << h) + val vbls = rop(g1, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, cb + 2 << h) + val s = vb >> 2 + if ( + s < 100 || { + dv = (s * 3435973837L >>> 35).toInt // divide a positive int by 10 + val sp40 = dv * 40 + val upin = vbls - sp40 + ((sp40 + vbrd + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + ) { + val s4 = s << 2 + val uin = vbls - s4 + dv = (~ { + if (((s4 + vbrd + 4) ^ uin) < 0) uin + else (vb & 0x3) + (s & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv.toLong) + exp += len - 1 + if (exp < -3 || exp >= 7) { + val dotOff = s.length + 1 + val sdv = stripTrailingZeros(dv) + s.append(sdv) + if (sdv < 10) s.append('0') + s.insert(dotOff, '.').append('E').append(exp) + } else if (exp < 0) { + s.append('0').append('.') + while ({ + exp += 1 + exp != 0 + }) s.append('0') + s.append(stripTrailingZeros(dv)) + } else if (exp + 1 < len) { + val dotOff = s.length + exp + 1 + s.append(stripTrailingZeros(dv)) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + s.toString + } + } + + private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { + val x = Math.multiplyHigh(g0, cp) + (g1 * cp >>> 1) + Math.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63 + } + + private[this] def rop(g: Long, cp: Int): Int = { + val x = Math.multiplyHigh(g, cp.toLong << 32) + (x >>> 31).toInt | -x.toInt >>> 31 + } + + private[this] def stripTrailingZeros(x: Long): Long = { + var q0 = x.toInt + if ( + q0 == x || { + q0 = (Math.multiplyHigh(x, 6189700196426901375L) >>> 25).toInt // divide a positive long by 100000000 + (x - q0 * 100000000L).toInt == 0 + } + ) return stripTrailingZeros(q0).toLong + var y = x + var q1, r1 = 0L + while ({ + q1 = y / 100 + r1 = y - q1 * 100 + r1 == 0 + }) y = q1 + q1 = y / 10 + r1 = y - q1 * 10 + if (r1 == 0) return q1 + y + } + + private[this] def stripTrailingZeros(x: Int): Int = { + var q0 = x + var q1 = 0 + while ({ + val qp = q0 * 1374389535L + q1 = (qp >> 37).toInt // divide a positive int by 100 + (qp & 0x1fc0000000L) == 0 // check if q is divisible by 100 + }) q0 = q1 + val qp = q0 * 3435973837L + q1 = (qp >> 35).toInt // divide a positive int by 10 + if ((qp & 0x7e0000000L) == 0) return q1 // check if q is divisible by 10 + q0 + } + + // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: + // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ + private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt + + private[this] val offsets = Array( + 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, + 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 4889916394579099648L, 4889916394579099648L, + 4889916394579099648L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, + 4323355642275676160L, 4323355642275676160L, 4323355642275676160L, 4035215266123964416L, 4035215266123964416L, + 4035215266123964416L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, + 3458764413820540928L, 3458764413820540928L, 3458764413820540928L, 3170534127668829184L, 3170534127668829184L, + 3170534127668829184L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, + 2594073385265405696L, 2594073385265405696L, 2594073385265405696L, 2305843009203693952L, 2305843009203693952L, + 2305843009203693952L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, + 1729382256910170464L, 1729382256910170464L, 1729382256910170464L, 1441151880758548720L, 1441151880758548720L, + 1441151880758548720L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, + 864691128455135132L, 864691128455135132L, 864691128455135132L, 576460752303423478L, 576460752303423478L, + 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L + ) + + private[this] val gs: Array[Long] = Array( + 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, + 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, + 7466108948025751031L, 8492109508320019073L, 5972887158420600825L, 4949013199285060097L, 4778309726736480660L, + 3959210559428048077L, 7645295562778369056L, 6334736895084876923L, 6116236450222695245L, 3223115108696946377L, + 4892989160178156196L, 2578492086957557102L, 7828782656285049914L, 436238524390181040L, 6263026125028039931L, + 2193665226883099993L, 5010420900022431944L, 9133629810990300641L, 8016673440035891111L, 9079784475471615541L, + 6413338752028712889L, 5419153173006337271L, 5130671001622970311L, 6179996945776024979L, 8209073602596752498L, + 6198646298499729642L, 6567258882077401998L, 8648265853541694037L, 5253807105661921599L, 1384589460720489745L, + 8406091369059074558L, 5904691951894693915L, 6724873095247259646L, 8413102376257665455L, 5379898476197807717L, + 4885807493635177203L, 8607837561916492348L, 438594360332462878L, 6886270049533193878L, 4040224303007880625L, + 5509016039626555102L, 6921528257148214824L, 8814425663402488164L, 3695747581953323071L, 7051540530721990531L, + 4801272472933613619L, 5641232424577592425L, 1996343570975935733L, 9025971879324147880L, 3194149713561497173L, + 7220777503459318304L, 2555319770849197738L, 5776622002767454643L, 3888930224050313352L, 4621297602213963714L, + 6800492993982161005L, 7394076163542341943L, 5346765568258592123L, 5915260930833873554L, 7966761269348784022L, + 4732208744667098843L, 8218083422849982379L, 7571533991467358150L, 2080887032334240837L, 6057227193173886520L, + 1664709625867392670L, 4845781754539109216L, 1331767700693914136L, 7753250807262574745L, 7664851543223128102L, + 6202600645810059796L, 6131881234578502482L, 4962080516648047837L, 3060830580291846824L, 7939328826636876539L, + 6742003335837910079L, 6351463061309501231L, 7238277076041283225L, 5081170449047600985L, 3945947253462071419L, + 8129872718476161576L, 6313515605539314269L, 6503898174780929261L, 3206138077060496254L, 5203118539824743409L, + 720236054277441842L, 8324989663719589454L, 4841726501585817270L, 6659991730975671563L, 5718055608639608977L, + 5327993384780537250L, 8263793301653597505L, 8524789415648859601L, 3998697245790980200L, 6819831532519087681L, + 1354283389261828999L, 5455865226015270144L, 8462124340893283845L, 8729384361624432231L, 8005375723316388668L, + 6983507489299545785L, 4559626171282155773L, 5586805991439636628L, 3647700937025724618L, 8938889586303418605L, + 3991647091870204227L, 7151111669042734884L, 3193317673496163382L, 5720889335234187907L, 4399328546167885867L, + 9153422936374700651L, 8883600081239572549L, 7322738349099760521L, 5262205657620702877L, 5858190679279808417L, + 2365090118725607140L, 4686552543423846733L, 7426095317093351197L, 7498484069478154774L, 813706063123630946L, + 5998787255582523819L, 2495639257869859918L, 4799029804466019055L, 3841185813666843096L, 7678447687145630488L, + 6145897301866948954L, 6142758149716504390L, 8606066656235469486L, 4914206519773203512L, 6884853324988375589L, + 7862730431637125620L, 3637067690497580296L, 6290184345309700496L, 2909654152398064237L, 5032147476247760397L, + 483048914547496228L, 8051435961996416635L, 2617552670646949126L, 6441148769597133308L, 2094042136517559301L, + 5152919015677706646L, 5364582523955957764L, 8244670425084330634L, 4893983223587622099L, 6595736340067464507L, + 5759860986241052841L, 5276589072053971606L, 918539974250931950L, 8442542515286354569L, 7003687180914356604L, + 6754034012229083655L, 7447624152102440445L, 5403227209783266924L, 5958099321681952356L, 8645163535653227079L, + 3998935692578258285L, 6916130828522581663L, 5043822961433561789L, 5532904662818065330L, 7724407183888759755L, + 8852647460508904529L, 3135679457367239799L, 7082117968407123623L, 4353217973264747001L, 5665694374725698898L, + 7171923193353707924L, 9065110999561118238L, 407030665140201709L, 7252088799648894590L, 4014973346854071690L, + 5801671039719115672L, 3211978677483257352L, 4641336831775292537L, 8103606164099471367L, 7426138930840468060L, + 5587072233075333540L, 5940911144672374448L, 4469657786460266832L, 4752728915737899558L, 7265075043910123789L, + 7604366265180639294L, 556073626030467093L, 6083493012144511435L, 2289533308195328836L, 4866794409715609148L, + 1831626646556263069L, 7786871055544974637L, 1085928227119065748L, 6229496844435979709L, 6402765803808118083L, + 4983597475548783767L, 6966887050417449628L, 7973755960878054028L, 3768321651184098759L, 6379004768702443222L, + 6704006135689189330L, 5103203814961954578L, 1673856093809441141L, 8165126103939127325L, 833495342724150664L, + 6532100883151301860L, 666796274179320531L, 5225680706521041488L, 533437019343456425L, 8361089130433666380L, + 8232196860433350926L, 6688871304346933104L, 6585757488346680741L, 5351097043477546483L, 7113280398048299755L, + 8561755269564074374L, 313202192651548637L, 6849404215651259499L, 2095236161492194072L, 5479523372521007599L, + 3520863336564710419L, 8767237396033612159L, 99358116390671185L, 7013789916826889727L, 1924160900483492110L, + 5611031933461511781L, 7073351942499659173L, 8977651093538418850L, 7628014293257544353L, 7182120874830735080L, + 6102411434606035483L, 5745696699864588064L, 4881929147684828386L, 9193114719783340903L, 2277063414182859933L, + 7354491775826672722L, 5510999546088198270L, 5883593420661338178L, 719450822128648293L, 4706874736529070542L, + 4264909472444828957L, 7530999578446512867L, 8668529563282681493L, 6024799662757210294L, 3245474835884234871L, + 4819839730205768235L, 4441054276078343059L, 7711743568329229176L, 7105686841725348894L, 6169394854663383341L, + 3839875066009323953L, 4935515883730706673L, 1227225645436504001L, 7896825413969130677L, 118886625327451240L, + 6317460331175304541L, 5629132522374826477L, 5053968264940243633L, 2658631610528906020L, 8086349223904389813L, + 2409136169475294470L, 6469079379123511850L, 5616657750322145900L, 5175263503298809480L, 4493326200257716720L, + 8280421605278095168L, 7189321920412346751L, 6624337284222476135L, 217434314217011916L, 5299469827377980908L, + 173947451373609533L, 8479151723804769452L, 7657013551681595899L, 6783321379043815562L, 2436262026603366396L, + 5426657103235052449L, 7483032843395558602L, 8682651365176083919L, 6438829327320028278L, 6946121092140867135L, + 6995737869226977784L, 5556896873712693708L, 5596590295381582227L, 8891034997940309933L, 7109870065239576402L, + 7112827998352247947L, 153872830078795637L, 5690262398681798357L, 5657121486175901994L, 9104419837890877372L, + 1672696748397622544L, 7283535870312701897L, 6872180620830963520L, 5826828696250161518L, 1808395681922860493L, + 4661462957000129214L, 5136065360280198718L, 7458340731200206743L, 2683681354335452463L, 5966672584960165394L, + 5836293898210272294L, 4773338067968132315L, 6513709525939172997L, 7637340908749011705L, 1198563204647900987L, + 6109872726999209364L, 958850563718320789L, 4887898181599367491L, 2611754858345611793L, 7820637090558987986L, + 489458958611068546L, 6256509672447190388L, 7770264796372675483L, 5005207737957752311L, 682188614985274902L, + 8008332380732403697L, 6625525006089305327L, 6406665904585922958L, 1611071190129533939L, 5125332723668738366L, + 4978205766845537474L, 8200532357869981386L, 4275780412210949635L, 6560425886295985109L, 1575949922397804547L, + 5248340709036788087L, 3105434345289198799L, 8397345134458860939L, 6813369359833673240L, 6717876107567088751L, + 7295369895237893754L, 5374300886053671001L, 3991621508819359841L, 8598881417685873602L, 2697245599369065423L, + 6879105134148698881L, 7691819701608117823L, 5503284107318959105L, 4308781353915539097L, 8805254571710334568L, + 6894050166264862555L, 7044203657368267654L, 9204588947753800367L, 5635362925894614123L, 9208345565573995455L, + 9016580681431382598L, 3665306460692661759L, 7213264545145106078L, 6621593983296039730L, 5770611636116084862L, + 8986624001378742108L, 4616489308892867890L, 3499950386361083363L, 7386382894228588624L, 5599920618177733380L, + 5909106315382870899L, 6324610901913141866L, 4727285052306296719L, 6904363128901468655L, 7563656083690074751L, + 5512957784129484362L, 6050924866952059801L, 2565691819932632328L, 4840739893561647841L, 207879048575150701L, + 7745183829698636545L, 5866629699833106606L, 6196147063758909236L, 4693303759866485285L, 4956917651007127389L, + 1909968600522233067L, 7931068241611403822L, 6745298575577483229L, 6344854593289123058L, 1706890045720076260L, + 5075883674631298446L, 5054860851317971332L, 8121413879410077514L, 4398428547366843807L, 6497131103528062011L, + 5363417245264430207L, 5197704882822449609L, 2446059388840589004L, 8316327812515919374L, 7603043836886852730L, + 6653062250012735499L, 7927109476880437346L, 5322449800010188399L, 8186361988875305038L, 8515919680016301439L, + 7564155960087622576L, 6812735744013041151L, 7895999175441053223L, 5450188595210432921L, 4472124932981887417L, + 8720301752336692674L, 3466051078029109543L, 6976241401869354139L, 4617515269794242796L, 5580993121495483311L, + 5538686623206349399L, 8929588994392773298L, 5172549782388248714L, 7143671195514218638L, 7827388640652509295L, + 5714936956411374911L, 727887690409141951L, 9143899130258199857L, 6698643526767492606L, 7315119304206559886L, + 1669566006672083762L, 5852095443365247908L, 8714350434821487656L, 4681676354692198327L, 1437457125744324640L, + 7490682167507517323L, 4144605808561874585L, 5992545734006013858L, 7005033461591409992L, 4794036587204811087L, + 70003547160262509L, 7670458539527697739L, 1956680082827375175L, 6136366831622158191L, 3410018473632855302L, + 4909093465297726553L, 883340371535329080L, 7854549544476362484L, 8792042223940347174L, 6283639635581089987L, + 8878308186523232901L, 5026911708464871990L, 3413297734476675998L, 8043058733543795184L, 5461276375162681596L, + 6434446986835036147L, 6213695507501100438L, 5147557589468028918L, 1281607591258970028L, 8236092143148846269L, + 205897738643396882L, 6588873714519077015L, 2009392598285672668L, 5271098971615261612L, 1607514078628538134L, + 8433758354584418579L, 4416696933176616176L, 6747006683667534863L, 5378031953912248102L, 5397605346934027890L, + 7991774377871708805L, 8636168555094444625L, 3563466967739958280L, 6908934844075555700L, 2850773574191966624L, + 5527147875260444560L, 2280618859353573299L, 8843436600416711296L, 3648990174965717279L, 7074749280333369037L, + 1074517732601618662L, 5659799424266695229L, 6393637408194160414L, 9055679078826712367L, 4695796630997791177L, + 7244543263061369894L, 67288490056322619L, 5795634610449095915L, 1898505199416013257L, 4636507688359276732L, + 1518804159532810606L, 7418412301374842771L, 4274761062623452130L, 5934729841099874217L, 1575134442727806543L, + 4747783872879899373L, 6794130776295110719L, 7596454196607838997L, 9025934834701221989L, 6077163357286271198L, + 3531399053019067268L, 4861730685829016958L, 6514468057157164137L, 7778769097326427133L, 8578474484080507458L, + 6223015277861141707L, 1328756365151540482L, 4978412222288913365L, 6597028314234097870L, 7965459555662261385L, + 1331873265919780784L, 6372367644529809108L, 1065498612735824627L, 5097894115623847286L, 4541747704930570025L, + 8156630584998155658L, 3577447513147001717L, 6525304467998524526L, 6551306825259511697L, 5220243574398819621L, + 3396371052836654196L, 8352389719038111394L, 1744844869796736390L, 6681911775230489115L, 3240550303208344274L, + 5345529420184391292L, 2592440242566675419L, 8552847072295026067L, 5992578795477635832L, 6842277657836020854L, + 1104714221640198342L, 5473822126268816683L, 2728445784683113836L, 8758115402030106693L, 2520838848122026975L, + 7006492321624085354L, 5706019893239531903L, 5605193857299268283L, 6409490321962580684L, 8968310171678829253L, + 8410510107769173933L, 7174648137343063403L, 1194384864102473662L, 5739718509874450722L, 4644856706023889253L, + 9183549615799121156L, 53073100154402158L, 7346839692639296924L, 7421156109607342373L, 5877471754111437539L, + 7781599295056829060L, 4701977403289150031L, 8069953843416418410L, 7523163845262640050L, 9222577334724359132L, + 6018531076210112040L, 7378061867779487306L, 4814824860968089632L, 5902449494223589845L, 7703719777548943412L, + 2065221561273923105L, 6162975822039154729L, 7186200471132003969L, 4930380657631323783L, 7593634784276558337L, + 7888609052210118054L, 1081769210616762369L, 6310887241768094443L, 2710089775864365057L, 5048709793414475554L, + 5857420635433402369L, 8077935669463160887L, 3837849794580578305L, 6462348535570528709L, 8604303057777328129L, + 5169878828456422967L, 8728116853592817665L, 8271806125530276748L, 6586289336264687617L, 6617444900424221398L, + 8958380283753660417L, 5293955920339377119L, 1632681004890062849L, 8470329472543003390L, 6301638422566010881L, + 6776263578034402712L, 5041310738052808705L, 5421010862427522170L, 343699775700336641L, 8673617379884035472L, + 549919641120538625L, 6938893903907228377L, 5973958935009296385L, 5551115123125782702L, 1089818333265526785L, + 8881784197001252323L, 3588383740595798017L, 7105427357601001858L, 6560055807218548737L, 5684341886080801486L, + 8937393460516749313L, 9094947017729282379L, 1387108685230112769L, 7275957614183425903L, 2954361355555045377L, + 5820766091346740722L, 6052837899185946625L, 4656612873077392578L, 1152921504606846977L, 7450580596923828125L, 1L, + 5960464477539062500L, 1L, 4768371582031250000L, 1L, 7629394531250000000L, 1L, 6103515625000000000L, 1L, + 4882812500000000000L, 1L, 7812500000000000000L, 1L, 6250000000000000000L, 1L, 5000000000000000000L, 1L, + 8000000000000000000L, 1L, 6400000000000000000L, 1L, 5120000000000000000L, 1L, 8192000000000000000L, 1L, + 6553600000000000000L, 1L, 5242880000000000000L, 1L, 8388608000000000000L, 1L, 6710886400000000000L, 1L, + 5368709120000000000L, 1L, 8589934592000000000L, 1L, 6871947673600000000L, 1L, 5497558138880000000L, 1L, + 8796093022208000000L, 1L, 7036874417766400000L, 1L, 5629499534213120000L, 1L, 9007199254740992000L, 1L, + 7205759403792793600L, 1L, 5764607523034234880L, 1L, 4611686018427387904L, 1L, 7378697629483820646L, + 3689348814741910324L, 5902958103587056517L, 1106804644422573097L, 4722366482869645213L, 6419466937650923963L, + 7555786372591432341L, 8426472692870523179L, 6044629098073145873L, 4896503746925463381L, 4835703278458516698L, + 7606551812282281028L, 7737125245533626718L, 1102436455425918676L, 6189700196426901374L, 4571297979082645264L, + 4951760157141521099L, 5501712790637071373L, 7922816251426433759L, 3268717242906448711L, 6338253001141147007L, + 4459648201696114131L, 5070602400912917605L, 9101741783469756789L, 8112963841460668169L, 5339414816696835055L, + 6490371073168534535L, 6116206260728423206L, 5192296858534827628L, 4892965008582738565L, 8307674973655724205L, + 5984069606361426541L, 6646139978924579364L, 4787255685089141233L, 5316911983139663491L, 5674478955442268148L, + 8507059173023461586L, 5389817513965718714L, 6805647338418769269L, 2467179603801619810L, 5444517870735015415L, + 3818418090412251009L, 8711228593176024664L, 6109468944659601615L, 6968982874540819731L, 6732249563098636453L, + 5575186299632655785L, 3541125243107954001L, 8920298079412249256L, 5665800388972726402L, 7136238463529799405L, + 2687965903807225960L, 5708990770823839524L, 2150372723045780768L, 9134385233318143238L, 7129945171615159552L, + 7307508186654514591L, 169932915179262157L, 5846006549323611672L, 7514643961627230372L, 4676805239458889338L, + 2322366354559873974L, 7482888383134222941L, 1871111759924843197L, 5986310706507378352L, 8875587037423695204L, + 4789048565205902682L, 3411120815197045840L, 7662477704329444291L, 7302467711686228506L, 6129982163463555433L, + 3997299761978027643L, 4903985730770844346L, 6887188624324332438L, 7846377169233350954L, 7330152984177021577L, + 6277101735386680763L, 7708796794712572423L, 5021681388309344611L, 633014213657192454L, 8034690221294951377L, + 6546845963964373411L, 6427752177035961102L, 1548127956429588405L, 5142201741628768881L, 6772525587256536209L, + 8227522786606030210L, 7146692124868547611L, 6582018229284824168L, 5717353699894838089L, 5265614583427859334L, + 8263231774657780795L, 8424983333484574935L, 7687147617339583786L, 6739986666787659948L, 6149718093871667029L, + 5391989333430127958L, 8609123289839243947L, 8627182933488204734L, 2706550819517059345L, 6901746346790563787L, + 4009915062984602637L, 5521397077432451029L, 8741955272500547595L, 8834235323891921647L, 8453105213888010667L, + 7067388259113537318L, 3073135356368498210L, 5653910607290829854L, 6147857099836708891L, 9046256971665327767L, + 4302548137625868741L, 7237005577332262213L, 8976061732213560478L, 5789604461865809771L, 1646826163657982898L, + 4631683569492647816L, 8696158560410206965L, 7410693711188236507L, 1001132845059645012L, 5928554968950589205L, + 6334929498160581494L, 4742843975160471364L, 5067943598528465196L, 7588550360256754183L, 2574686535532678828L, + 6070840288205403346L, 5749098043168053386L, 4856672230564322677L, 2754604027163487547L, 7770675568902916283L, + 6252040850832535236L, 6216540455122333026L, 8690981495407938512L, 4973232364097866421L, 5108110788955395648L, + 7957171782556586274L, 4483628447586722714L, 6365737426045269019L, 5431577165440333333L, 5092589940836215215L, + 6189936139723221828L, 8148143905337944345L, 680525786702379117L, 6518515124270355476L, 544420629361903293L, + 5214812099416284380L, 7814234132973343281L, 8343699359066055009L, 3279402575902573442L, 6674959487252844007L, + 4468196468093013915L, 5339967589802275205L, 9108580396587276617L, 8543948143683640329L, 5350356597684866779L, + 6835158514946912263L, 6124959685518848585L, 5468126811957529810L, 8589316563156989191L, 8749002899132047697L, + 4519534464196406897L, 6999202319305638157L, 9149650793469991003L, 5599361855444510526L, 3630371820034082479L, + 8958978968711216842L, 2119246097312621643L, 7167183174968973473L, 7229420099962962799L, 5733746539975178779L, + 249512857857504755L, 9173994463960286046L, 4088569387313917931L, 7339195571168228837L, 1426181102480179183L, + 5871356456934583069L, 6674968104097008831L, 4697085165547666455L, 7184648890648562227L, 7515336264876266329L, + 2272066188182923754L, 6012269011901013063L, 3662327357917294165L, 4809815209520810450L, 6619210701075745655L, + 7695704335233296721L, 1367365084866417240L, 6156563468186637376L, 8472589697376954439L, 4925250774549309901L, + 4933397350530608390L, 7880401239278895842L, 4204086946107063100L, 6304320991423116673L, 8897292778998515965L, + 5043456793138493339L, 1583811001085947287L, 8069530869021589342L, 6223446416479425982L, 6455624695217271474L, + 1289408318441630463L, 5164499756173817179L, 2876201062124259532L, 8263199609878107486L, 8291270514140725574L, + 6610559687902485989L, 4788342003941625298L, 5288447750321988791L, 5675348010524255400L, 8461516400515182066L, + 5391208002096898316L, 6769213120412145653L, 2468291994306563491L, 5415370496329716522L, 5663982410187161116L, + 8664592794127546436L, 1683674226815637140L, 6931674235302037148L, 8725637010936330358L, 5545339388241629719L, + 1446486386636198802L, 8872543021186607550L, 6003727033359828406L, 7098034416949286040L, 4802981626687862725L, + 5678427533559428832L, 3842385301350290180L, 9085484053695086131L, 7992490889531419449L, 7268387242956068905L, + 4549318304254180398L, 5814709794364855124L, 3639454643403344318L, 4651767835491884099L, 4756238122093630616L, + 7442828536787014559L, 2075957773236943501L, 5954262829429611647L, 3505440625960509963L, 4763410263543689317L, + 8338375722881273455L, 7621456421669902908L, 5962703527126216881L, 6097165137335922326L, 8459511636442883828L, + 4877732109868737861L, 4922934901783351901L, 7804371375789980578L, 4187347028111452718L, 6243497100631984462L, + 7039226437231072498L, 4994797680505587570L, 1942032335042947675L, 7991676288808940112L, 3107251736068716280L, + 6393341031047152089L, 8019824610967838509L, 5114672824837721671L, 8260534096145225969L, 8183476519740354675L, + 304133702235675419L, 6546781215792283740L, 243306961788540335L, 5237424972633826992L, 194645569430832268L, + 8379879956214123187L, 2156107318460286790L, 6703903964971298549L, 7258909076881094917L, 5363123171977038839L, + 7651801668875831096L, 8580997075163262143L, 6708859448088464268L, 6864797660130609714L, 9056436373212681737L, + 5491838128104487771L, 9089823505941100552L, 8786941004967180435L, 1630996757909074751L, 7029552803973744348L, + 1304797406327259801L, 5623642243178995478L, 4733186739803718164L, 8997827589086392765L, 5728424376314993901L, + 7198262071269114212L, 4582739501051995121L, 5758609657015291369L, 9200214822954461581L, 9213775451224466191L, + 9186320494614273045L, 7371020360979572953L, 5504381988320463275L, 5896816288783658362L, 8092854405398280943L, + 4717453031026926690L, 2784934709576714431L, 7547924849643082704L, 4455895535322743090L, 6038339879714466163L, + 5409390835629149634L, 4830671903771572930L, 8016861483245230030L, 7729075046034516689L, 3603606336337592240L, + 6183260036827613351L, 4727559476441028954L, 4946608029462090681L, 1937373173781868001L, 7914572847139345089L, + 8633820300163854287L, 6331658277711476071L, 8751730647502038591L, 5065326622169180857L, 5156710110630675711L, + 8104522595470689372L, 872038547525260492L, 6483618076376551497L, 6231654060133073878L, 5186894461101241198L, + 1295974433364548779L, 8299031137761985917L, 228884686012322885L, 6639224910209588733L, 5717130970922723793L, + 5311379928167670986L, 8263053591480089358L, 8498207885068273579L, 308164894771456841L, 6798566308054618863L, + 2091206323188120634L, 5438853046443695090L, 5362313873292406831L, 8702164874309912144L, 8579702197267850929L, + 6961731899447929715L, 8708436165185235905L, 5569385519558343772L, 6966748932148188724L, 8911016831293350036L, + 3768100661953281312L, 7128813465034680029L, 1169806122191669888L, 5703050772027744023L, 2780519305124291072L, + 9124881235244390437L, 2604156480827910553L, 7299904988195512349L, 7617348406775193928L, 5839923990556409879L, + 7938553132791110304L, 4671939192445127903L, 8195516913603843405L, 7475102707912204646L, 2044780617540418478L, + 5980082166329763716L, 9014522123516155429L, 4784065733063810973L, 5366943291441969181L, 7654505172902097557L, + 6742434858936195528L, 6123604138321678046L, 1704599072407046100L, 4898883310657342436L, 8742376887409457526L, + 7838213297051747899L, 1075082168258445910L, 6270570637641398319L, 2704740141977711890L, 5016456510113118655L, + 4008466520953124674L, 8026330416180989848L, 6413546433524999478L, 6421064332944791878L, 8820185961561909905L, + 5136851466355833503L, 1522125547136662440L, 8218962346169333605L, 590726468047704741L, 6575169876935466884L, + 472581174438163793L, 5260135901548373507L, 2222739346921486196L, 8416217442477397611L, 5401057362445333075L, + 6732973953981918089L, 2476171482585311299L, 5386379163185534471L, 3825611593439204201L, 8618206661096855154L, + 2431629734760816398L, 6894565328877484123L, 3789978195179608280L, 5515652263101987298L, 6721331370885596947L, + 8825043620963179677L, 8909455786045999954L, 7060034896770543742L, 3438215814094889640L, 5648027917416434993L, + 8284595873388777197L, 9036844667866295990L, 2187306953196312545L, 7229475734293036792L, 1749845562557050036L, + 5783580587434429433L, 6933899672158505514L, 4626864469947543547L, 13096515613938926L, 7402983151916069675L, + 1865628832353257443L, 5922386521532855740L, 1492503065882605955L, 4737909217226284592L, 1194002452706084764L, + 7580654747562055347L, 3755078331700690783L, 6064523798049644277L, 8538085887473418112L, 4851619038439715422L, + 3141119895236824166L, 7762590461503544675L, 6870466239749873827L, 6210072369202835740L, 5496372991799899062L, + 4968057895362268592L, 4397098393439919250L, 7948892632579629747L, 8880031836874825961L, 6359114106063703798L, + 3414676654757950445L, 5087291284850963038L, 6421090138548270680L, 8139666055761540861L, 8429069814306277926L, + 6511732844609232689L, 4898581444074067179L, 5209386275687386151L, 5763539562630208905L, 8335018041099817842L, + 5532314485466423924L, 6668014432879854274L, 736502773631228816L, 5334411546303883419L, 2433876626275938215L, + 8535058474086213470L, 7583551416783411467L, 6828046779268970776L, 6066841133426729173L, 5462437423415176621L, + 3008798499370428177L, 8739899877464282594L, 1124728784250774760L, 6991919901971426075L, 2744457434771574970L, + 5593535921577140860L, 2195565947817259976L, 8949657474523425376L, 3512905516507615961L, 7159725979618740301L, + 965650005835137607L, 5727780783694992240L, 8151217634151930732L, 9164449253911987585L, 3818576177788313364L, + 7331559403129590068L, 3054860942230650691L, 5865247522503672054L, 6133237568526430876L, 4692198018002937643L, + 6751264462192099863L, 7507516828804700229L, 8957348732136404618L, 6006013463043760183L, 9010553393080078856L, + 4804810770435008147L, 1674419492351197600L, 7687697232696013035L, 4523745595132871322L, 6150157786156810428L, + 3618996476106297057L, 4920126228925448342L, 6584545995626947969L, 7872201966280717348L, 3156575963519296104L, + 6297761573024573878L, 6214609585557347207L, 5038209258419659102L, 8661036483187788089L, 8061134813471454564L, + 6478960743616640295L, 6448907850777163651L, 7027843002264267398L, 5159126280621730921L, 3777599994440458757L, + 8254602048994769474L, 2354811176362823687L, 6603681639195815579L, 3728523348461214111L, 5282945311356652463L, + 4827493086139926451L, 8452712498170643941L, 5879314530452927160L, 6762169998536515153L, 2858777216991386566L, + 5409735998829212122L, 5976370588335019576L, 8655577598126739396L, 2183495311852210675L, 6924462078501391516L, + 9125493878965589187L, 5539569662801113213L, 5455720695801516188L, 8863311460481781141L, 6884478705911470739L, + 7090649168385424913L, 3662908557358221429L, 5672519334708339930L, 6619675660628487467L, 9076030935533343889L, + 1368109020150804139L, 7260824748426675111L, 2939161623491598473L, 5808659798741340089L, 506654891422323617L, + 4646927838993072071L, 2249998320508814055L, 7435084542388915313L, 9134020534926967972L, 5948067633911132251L, + 1773193205828708893L, 4758454107128905800L, 8797252194146787761L, 7613526571406249281L, 4852231473780084609L, + 6090821257124999425L, 2037110771653112526L, 4872657005699999540L, 1629688617322490021L, 7796251209119999264L, + 2607501787715984033L, 6237000967295999411L, 3930675837543742388L, 4989600773836799529L, 1299866262664038749L, + 7983361238138879246L, 5769134835004372321L, 6386688990511103397L, 2770633460632542696L, 5109351192408882717L, + 7750529990618899641L, 8174961907854212348L, 5022150355506418780L, 6539969526283369878L, 7707069099147045347L, + 5231975621026695903L, 631632057204770793L, 8371160993642713444L, 8389308921011453915L, 6696928794914170755L, + 8556121544180118293L, 5357543035931336604L, 6844897235344094635L, 8572068857490138567L, 5417812354437685931L, + 6857655085992110854L, 644901068808238421L, 5486124068793688683L, 2360595262417545899L, 8777798510069901893L, + 1932278012497118276L, 7022238808055921514L, 5235171224739604944L, 5617791046444737211L, 6032811387162639117L, + 8988465674311579538L, 5963149404718312264L, 7190772539449263630L, 8459868338516560134L, 5752618031559410904L, + 6767894670813248108L, 9204188850495057447L, 5294608251188331487L + ) +} diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala new file mode 100644 index 000000000..26f3c480a --- /dev/null +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -0,0 +1,571 @@ +/* + * Copyright 2019-2022 John A. De Goes and the ZIO Contributors + * + * 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 zio.json.internal + +/** + * Total, fast, number parsing. + * + * The Java and Scala standard libraries throw exceptions when we attempt to parse an invalid number. Unfortunately, + * exceptions are very expensive, and untrusted data can be maliciously constructed to DOS a server. + * + * This suite of functions mitigates against such attacks by building up the numbers one character at a time, which has + * been shown through extensive benchmarking to be orders of magnitude faster than exception-throwing stdlib parsers, + * for valid and invalid inputs. This approach, proposed by alexknvl, was also benchmarked against regexp-based + * pre-validation. + * + * Note that although the behaviour is identical to the Java stdlib when given the canonical form of a primitive (i.e. + * the .toString) of a number there may be differences in behaviour for non-canonical forms. e.g. the Java stdlib may + * reject "1.0" when parsed as an `BigInteger` but we may parse it as a `1`, although "1.1" would be rejected. Parsing + * of `BigDecimal` preserves the trailing zeros on the right but not on the left, e.g. "000.00001000" will be + * "1.000e-5", which is useful in cases where the trailing zeros denote measurement accuracy. + * + * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit limit on the size of the significand, to + * avoid OOM style attacks, which is 128 bits by default. + * + * Results are contained in a specialisation of Option that avoids boxing. + */ +object SafeNumbers { + import UnsafeNumbers.UnsafeNumber + + def byte(num: String): ByteOption = + try ByteSome(UnsafeNumbers.byte(num)) + catch { case UnsafeNumber => ByteNone } + + def short(num: String): ShortOption = + try ShortSome(UnsafeNumbers.short(num)) + catch { case UnsafeNumber => ShortNone } + + def int(num: String): IntOption = + try IntSome(UnsafeNumbers.int(num)) + catch { case UnsafeNumber => IntNone } + + def long(num: String): LongOption = + try LongSome(UnsafeNumbers.long(num)) + catch { case UnsafeNumber => LongNone } + + def bigInteger( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigInteger] = + try Some(UnsafeNumbers.bigInteger(num, max_bits)) + catch { case UnsafeNumber => None } + + def float(num: String, max_bits: Int = 128): FloatOption = + try FloatSome(UnsafeNumbers.float(num, max_bits)) + catch { case UnsafeNumber => FloatNone } + + def double(num: String, max_bits: Int = 128): DoubleOption = + try DoubleSome(UnsafeNumbers.double(num, max_bits)) + catch { case UnsafeNumber => DoubleNone } + + def bigDecimal( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigDecimal] = + try Some(UnsafeNumbers.bigDecimal(num, max_bits)) + catch { case UnsafeNumber => None } + + // Based on the amazing work of Raffaello Giulietti + // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view + // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java + def toString(x: Double): String = { + val bits = java.lang.Double.doubleToLongBits(x) + val ieeeExponent = (bits >> 52).toInt & 0x7ff + val ieeeMantissa = bits & 0xfffffffffffffL + if (ieeeExponent == 2047) { + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + } else { + val s = new java.lang.StringBuilder(24) + if (bits < 0) s.append('-') + if (x == 0.0f) s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 1075 + var m = ieeeMantissa | 0x10000000000000L + var dv = 0L + var exp = 0 + if (e == 0) dv = m + else if (e >= -52 && e < 0 && m << e == 0) dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if (ieeeExponent == 0) { + e = -1074 + m = ieeeMantissa + if (ieeeMantissa < 3) { + m *= 10 + expShift = 1 + } + } else if (ieeeMantissa == 0 && ieeeExponent > 1) { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val i = exp + 324 << 1 + val g1 = gs(i) + val g0 = gs(i + 1) + val h = (-exp * 108853 >> 15) + e + 2 + val cb = m << 2 + val outm1 = (m.toInt & 0x1) - 1 + val vb = rop(g1, g0, cb << h) + val vbls = rop(g1, g0, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, g0, cb + 2 << h) + val s = vb >> 2 + if ( + s < 100 || { + dv = s / 10 + val sp40 = dv * 40 + val upin = (vbls - sp40).toInt + (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + ) { + val s4 = s << 2 + val uin = (vbls - s4).toInt + dv = (~ { + if ((((s4 + vbrd).toInt + 4) ^ uin) < 0) uin + else (vb.toInt & 0x3) + (s.toInt & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv) + exp += len - 1 + if (exp < -3 || exp >= 7) { + val dotOff = s.length + 1 + val sdv = stripTrailingZeros(dv) + s.append(sdv) + if (sdv < 10) s.append('0') + s.insert(dotOff, '.').append('E').append(exp) + } else if (exp < 0) { + s.append('0').append('.') + while ({ + exp += 1 + exp != 0 + }) s.append('0') + s.append(stripTrailingZeros(dv)) + } else if (exp + 1 < len) { + val dotOff = s.length + exp + 1 + s.append(stripTrailingZeros(dv)) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + s.toString + } + } + + def toString(x: Float): String = { + val bits = java.lang.Float.floatToIntBits(x) + val ieeeExponent = (bits >> 23) & 0xff + val ieeeMantissa = bits & 0x7fffff + if (ieeeExponent == 255) { + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + } else { + val s = new java.lang.StringBuilder(16) + if (bits < 0) s.append('-') + if (x == 0.0f) s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 150 + var m = ieeeMantissa | 0x800000 + var dv, exp = 0 + if (e == 0) dv = m + else if (e >= -23 && e < 0 && m << e == 0) dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if (ieeeExponent == 0) { + e = -149 + m = ieeeMantissa + if (ieeeMantissa < 8) { + m *= 10 + expShift = 1 + } + } else if (ieeeMantissa == 0 && ieeeExponent > 1) { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val g1 = gs(exp + 324 << 1) + 1 + val h = (-exp * 108853 >> 15) + e + 1 + val cb = m << 2 + val outm1 = (m & 0x1) - 1 + val vb = rop(g1, cb << h) + val vbls = rop(g1, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, cb + 2 << h) + val s = vb >> 2 + if ( + s < 100 || { + dv = (s * 3435973837L >>> 35).toInt // divide a positive int by 10 + val sp40 = dv * 40 + val upin = vbls - sp40 + ((sp40 + vbrd + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + ) { + val s4 = s << 2 + val uin = vbls - s4 + dv = (~ { + if (((s4 + vbrd + 4) ^ uin) < 0) uin + else (vb & 0x3) + (s & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv.toLong) + exp += len - 1 + if (exp < -3 || exp >= 7) { + val dotOff = s.length + 1 + val sdv = stripTrailingZeros(dv) + s.append(sdv) + if (sdv < 10) s.append('0') + s.insert(dotOff, '.').append('E').append(exp) + } else if (exp < 0) { + s.append('0').append('.') + while ({ + exp += 1 + exp != 0 + }) s.append('0') + s.append(stripTrailingZeros(dv)) + } else if (exp + 1 < len) { + val dotOff = s.length + exp + 1 + s.append(stripTrailingZeros(dv)) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + s.toString + } + } + + private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { + val x = Math.multiplyHigh(g0, cp) + (g1 * cp >>> 1) + Math.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63 + } + + private[this] def rop(g: Long, cp: Int): Int = { + val x = ((g & 0xffffffffL) * cp >>> 32) + (g >>> 32) * cp + (x >>> 31).toInt | -x.toInt >>> 31 + } + + private[this] def stripTrailingZeros(x: Long): Long = { + var q0 = x.toInt + if ( + q0 == x || { + q0 = (x / 100000000L).toInt + (x - q0 * 100000000L).toInt == 0 + } + ) return stripTrailingZeros(q0).toLong + var y = x + var q1, r1 = 0L + while ({ + q1 = y / 100 + r1 = y - q1 * 100 + r1 == 0 + }) y = q1 + q1 = y / 10 + r1 = y - q1 * 10 + if (r1 == 0) return q1 + y + } + + private[this] def stripTrailingZeros(x: Int): Int = { + var q0 = x + var q1 = 0 + while ({ + val qp = q0 * 1374389535L + q1 = (qp >> 37).toInt // divide a positive int by 100 + (qp & 0x1fc0000000L) == 0 // check if q is divisible by 100 + }) q0 = q1 + val qp = q0 * 3435973837L + q1 = (qp >> 35).toInt // divide a positive int by 10 + if ((qp & 0x7e0000000L) == 0) return q1 // check if q is divisible by 10 + q0 + } + + // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: + // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ + private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt + + private[this] val offsets = Array( + 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, + 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 4889916394579099648L, 4889916394579099648L, + 4889916394579099648L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, + 4323355642275676160L, 4323355642275676160L, 4323355642275676160L, 4035215266123964416L, 4035215266123964416L, + 4035215266123964416L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, + 3458764413820540928L, 3458764413820540928L, 3458764413820540928L, 3170534127668829184L, 3170534127668829184L, + 3170534127668829184L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, + 2594073385265405696L, 2594073385265405696L, 2594073385265405696L, 2305843009203693952L, 2305843009203693952L, + 2305843009203693952L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, + 1729382256910170464L, 1729382256910170464L, 1729382256910170464L, 1441151880758548720L, 1441151880758548720L, + 1441151880758548720L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, + 864691128455135132L, 864691128455135132L, 864691128455135132L, 576460752303423478L, 576460752303423478L, + 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L + ) + + private[this] val gs: Array[Long] = Array( + 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, + 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, + 7466108948025751031L, 8492109508320019073L, 5972887158420600825L, 4949013199285060097L, 4778309726736480660L, + 3959210559428048077L, 7645295562778369056L, 6334736895084876923L, 6116236450222695245L, 3223115108696946377L, + 4892989160178156196L, 2578492086957557102L, 7828782656285049914L, 436238524390181040L, 6263026125028039931L, + 2193665226883099993L, 5010420900022431944L, 9133629810990300641L, 8016673440035891111L, 9079784475471615541L, + 6413338752028712889L, 5419153173006337271L, 5130671001622970311L, 6179996945776024979L, 8209073602596752498L, + 6198646298499729642L, 6567258882077401998L, 8648265853541694037L, 5253807105661921599L, 1384589460720489745L, + 8406091369059074558L, 5904691951894693915L, 6724873095247259646L, 8413102376257665455L, 5379898476197807717L, + 4885807493635177203L, 8607837561916492348L, 438594360332462878L, 6886270049533193878L, 4040224303007880625L, + 5509016039626555102L, 6921528257148214824L, 8814425663402488164L, 3695747581953323071L, 7051540530721990531L, + 4801272472933613619L, 5641232424577592425L, 1996343570975935733L, 9025971879324147880L, 3194149713561497173L, + 7220777503459318304L, 2555319770849197738L, 5776622002767454643L, 3888930224050313352L, 4621297602213963714L, + 6800492993982161005L, 7394076163542341943L, 5346765568258592123L, 5915260930833873554L, 7966761269348784022L, + 4732208744667098843L, 8218083422849982379L, 7571533991467358150L, 2080887032334240837L, 6057227193173886520L, + 1664709625867392670L, 4845781754539109216L, 1331767700693914136L, 7753250807262574745L, 7664851543223128102L, + 6202600645810059796L, 6131881234578502482L, 4962080516648047837L, 3060830580291846824L, 7939328826636876539L, + 6742003335837910079L, 6351463061309501231L, 7238277076041283225L, 5081170449047600985L, 3945947253462071419L, + 8129872718476161576L, 6313515605539314269L, 6503898174780929261L, 3206138077060496254L, 5203118539824743409L, + 720236054277441842L, 8324989663719589454L, 4841726501585817270L, 6659991730975671563L, 5718055608639608977L, + 5327993384780537250L, 8263793301653597505L, 8524789415648859601L, 3998697245790980200L, 6819831532519087681L, + 1354283389261828999L, 5455865226015270144L, 8462124340893283845L, 8729384361624432231L, 8005375723316388668L, + 6983507489299545785L, 4559626171282155773L, 5586805991439636628L, 3647700937025724618L, 8938889586303418605L, + 3991647091870204227L, 7151111669042734884L, 3193317673496163382L, 5720889335234187907L, 4399328546167885867L, + 9153422936374700651L, 8883600081239572549L, 7322738349099760521L, 5262205657620702877L, 5858190679279808417L, + 2365090118725607140L, 4686552543423846733L, 7426095317093351197L, 7498484069478154774L, 813706063123630946L, + 5998787255582523819L, 2495639257869859918L, 4799029804466019055L, 3841185813666843096L, 7678447687145630488L, + 6145897301866948954L, 6142758149716504390L, 8606066656235469486L, 4914206519773203512L, 6884853324988375589L, + 7862730431637125620L, 3637067690497580296L, 6290184345309700496L, 2909654152398064237L, 5032147476247760397L, + 483048914547496228L, 8051435961996416635L, 2617552670646949126L, 6441148769597133308L, 2094042136517559301L, + 5152919015677706646L, 5364582523955957764L, 8244670425084330634L, 4893983223587622099L, 6595736340067464507L, + 5759860986241052841L, 5276589072053971606L, 918539974250931950L, 8442542515286354569L, 7003687180914356604L, + 6754034012229083655L, 7447624152102440445L, 5403227209783266924L, 5958099321681952356L, 8645163535653227079L, + 3998935692578258285L, 6916130828522581663L, 5043822961433561789L, 5532904662818065330L, 7724407183888759755L, + 8852647460508904529L, 3135679457367239799L, 7082117968407123623L, 4353217973264747001L, 5665694374725698898L, + 7171923193353707924L, 9065110999561118238L, 407030665140201709L, 7252088799648894590L, 4014973346854071690L, + 5801671039719115672L, 3211978677483257352L, 4641336831775292537L, 8103606164099471367L, 7426138930840468060L, + 5587072233075333540L, 5940911144672374448L, 4469657786460266832L, 4752728915737899558L, 7265075043910123789L, + 7604366265180639294L, 556073626030467093L, 6083493012144511435L, 2289533308195328836L, 4866794409715609148L, + 1831626646556263069L, 7786871055544974637L, 1085928227119065748L, 6229496844435979709L, 6402765803808118083L, + 4983597475548783767L, 6966887050417449628L, 7973755960878054028L, 3768321651184098759L, 6379004768702443222L, + 6704006135689189330L, 5103203814961954578L, 1673856093809441141L, 8165126103939127325L, 833495342724150664L, + 6532100883151301860L, 666796274179320531L, 5225680706521041488L, 533437019343456425L, 8361089130433666380L, + 8232196860433350926L, 6688871304346933104L, 6585757488346680741L, 5351097043477546483L, 7113280398048299755L, + 8561755269564074374L, 313202192651548637L, 6849404215651259499L, 2095236161492194072L, 5479523372521007599L, + 3520863336564710419L, 8767237396033612159L, 99358116390671185L, 7013789916826889727L, 1924160900483492110L, + 5611031933461511781L, 7073351942499659173L, 8977651093538418850L, 7628014293257544353L, 7182120874830735080L, + 6102411434606035483L, 5745696699864588064L, 4881929147684828386L, 9193114719783340903L, 2277063414182859933L, + 7354491775826672722L, 5510999546088198270L, 5883593420661338178L, 719450822128648293L, 4706874736529070542L, + 4264909472444828957L, 7530999578446512867L, 8668529563282681493L, 6024799662757210294L, 3245474835884234871L, + 4819839730205768235L, 4441054276078343059L, 7711743568329229176L, 7105686841725348894L, 6169394854663383341L, + 3839875066009323953L, 4935515883730706673L, 1227225645436504001L, 7896825413969130677L, 118886625327451240L, + 6317460331175304541L, 5629132522374826477L, 5053968264940243633L, 2658631610528906020L, 8086349223904389813L, + 2409136169475294470L, 6469079379123511850L, 5616657750322145900L, 5175263503298809480L, 4493326200257716720L, + 8280421605278095168L, 7189321920412346751L, 6624337284222476135L, 217434314217011916L, 5299469827377980908L, + 173947451373609533L, 8479151723804769452L, 7657013551681595899L, 6783321379043815562L, 2436262026603366396L, + 5426657103235052449L, 7483032843395558602L, 8682651365176083919L, 6438829327320028278L, 6946121092140867135L, + 6995737869226977784L, 5556896873712693708L, 5596590295381582227L, 8891034997940309933L, 7109870065239576402L, + 7112827998352247947L, 153872830078795637L, 5690262398681798357L, 5657121486175901994L, 9104419837890877372L, + 1672696748397622544L, 7283535870312701897L, 6872180620830963520L, 5826828696250161518L, 1808395681922860493L, + 4661462957000129214L, 5136065360280198718L, 7458340731200206743L, 2683681354335452463L, 5966672584960165394L, + 5836293898210272294L, 4773338067968132315L, 6513709525939172997L, 7637340908749011705L, 1198563204647900987L, + 6109872726999209364L, 958850563718320789L, 4887898181599367491L, 2611754858345611793L, 7820637090558987986L, + 489458958611068546L, 6256509672447190388L, 7770264796372675483L, 5005207737957752311L, 682188614985274902L, + 8008332380732403697L, 6625525006089305327L, 6406665904585922958L, 1611071190129533939L, 5125332723668738366L, + 4978205766845537474L, 8200532357869981386L, 4275780412210949635L, 6560425886295985109L, 1575949922397804547L, + 5248340709036788087L, 3105434345289198799L, 8397345134458860939L, 6813369359833673240L, 6717876107567088751L, + 7295369895237893754L, 5374300886053671001L, 3991621508819359841L, 8598881417685873602L, 2697245599369065423L, + 6879105134148698881L, 7691819701608117823L, 5503284107318959105L, 4308781353915539097L, 8805254571710334568L, + 6894050166264862555L, 7044203657368267654L, 9204588947753800367L, 5635362925894614123L, 9208345565573995455L, + 9016580681431382598L, 3665306460692661759L, 7213264545145106078L, 6621593983296039730L, 5770611636116084862L, + 8986624001378742108L, 4616489308892867890L, 3499950386361083363L, 7386382894228588624L, 5599920618177733380L, + 5909106315382870899L, 6324610901913141866L, 4727285052306296719L, 6904363128901468655L, 7563656083690074751L, + 5512957784129484362L, 6050924866952059801L, 2565691819932632328L, 4840739893561647841L, 207879048575150701L, + 7745183829698636545L, 5866629699833106606L, 6196147063758909236L, 4693303759866485285L, 4956917651007127389L, + 1909968600522233067L, 7931068241611403822L, 6745298575577483229L, 6344854593289123058L, 1706890045720076260L, + 5075883674631298446L, 5054860851317971332L, 8121413879410077514L, 4398428547366843807L, 6497131103528062011L, + 5363417245264430207L, 5197704882822449609L, 2446059388840589004L, 8316327812515919374L, 7603043836886852730L, + 6653062250012735499L, 7927109476880437346L, 5322449800010188399L, 8186361988875305038L, 8515919680016301439L, + 7564155960087622576L, 6812735744013041151L, 7895999175441053223L, 5450188595210432921L, 4472124932981887417L, + 8720301752336692674L, 3466051078029109543L, 6976241401869354139L, 4617515269794242796L, 5580993121495483311L, + 5538686623206349399L, 8929588994392773298L, 5172549782388248714L, 7143671195514218638L, 7827388640652509295L, + 5714936956411374911L, 727887690409141951L, 9143899130258199857L, 6698643526767492606L, 7315119304206559886L, + 1669566006672083762L, 5852095443365247908L, 8714350434821487656L, 4681676354692198327L, 1437457125744324640L, + 7490682167507517323L, 4144605808561874585L, 5992545734006013858L, 7005033461591409992L, 4794036587204811087L, + 70003547160262509L, 7670458539527697739L, 1956680082827375175L, 6136366831622158191L, 3410018473632855302L, + 4909093465297726553L, 883340371535329080L, 7854549544476362484L, 8792042223940347174L, 6283639635581089987L, + 8878308186523232901L, 5026911708464871990L, 3413297734476675998L, 8043058733543795184L, 5461276375162681596L, + 6434446986835036147L, 6213695507501100438L, 5147557589468028918L, 1281607591258970028L, 8236092143148846269L, + 205897738643396882L, 6588873714519077015L, 2009392598285672668L, 5271098971615261612L, 1607514078628538134L, + 8433758354584418579L, 4416696933176616176L, 6747006683667534863L, 5378031953912248102L, 5397605346934027890L, + 7991774377871708805L, 8636168555094444625L, 3563466967739958280L, 6908934844075555700L, 2850773574191966624L, + 5527147875260444560L, 2280618859353573299L, 8843436600416711296L, 3648990174965717279L, 7074749280333369037L, + 1074517732601618662L, 5659799424266695229L, 6393637408194160414L, 9055679078826712367L, 4695796630997791177L, + 7244543263061369894L, 67288490056322619L, 5795634610449095915L, 1898505199416013257L, 4636507688359276732L, + 1518804159532810606L, 7418412301374842771L, 4274761062623452130L, 5934729841099874217L, 1575134442727806543L, + 4747783872879899373L, 6794130776295110719L, 7596454196607838997L, 9025934834701221989L, 6077163357286271198L, + 3531399053019067268L, 4861730685829016958L, 6514468057157164137L, 7778769097326427133L, 8578474484080507458L, + 6223015277861141707L, 1328756365151540482L, 4978412222288913365L, 6597028314234097870L, 7965459555662261385L, + 1331873265919780784L, 6372367644529809108L, 1065498612735824627L, 5097894115623847286L, 4541747704930570025L, + 8156630584998155658L, 3577447513147001717L, 6525304467998524526L, 6551306825259511697L, 5220243574398819621L, + 3396371052836654196L, 8352389719038111394L, 1744844869796736390L, 6681911775230489115L, 3240550303208344274L, + 5345529420184391292L, 2592440242566675419L, 8552847072295026067L, 5992578795477635832L, 6842277657836020854L, + 1104714221640198342L, 5473822126268816683L, 2728445784683113836L, 8758115402030106693L, 2520838848122026975L, + 7006492321624085354L, 5706019893239531903L, 5605193857299268283L, 6409490321962580684L, 8968310171678829253L, + 8410510107769173933L, 7174648137343063403L, 1194384864102473662L, 5739718509874450722L, 4644856706023889253L, + 9183549615799121156L, 53073100154402158L, 7346839692639296924L, 7421156109607342373L, 5877471754111437539L, + 7781599295056829060L, 4701977403289150031L, 8069953843416418410L, 7523163845262640050L, 9222577334724359132L, + 6018531076210112040L, 7378061867779487306L, 4814824860968089632L, 5902449494223589845L, 7703719777548943412L, + 2065221561273923105L, 6162975822039154729L, 7186200471132003969L, 4930380657631323783L, 7593634784276558337L, + 7888609052210118054L, 1081769210616762369L, 6310887241768094443L, 2710089775864365057L, 5048709793414475554L, + 5857420635433402369L, 8077935669463160887L, 3837849794580578305L, 6462348535570528709L, 8604303057777328129L, + 5169878828456422967L, 8728116853592817665L, 8271806125530276748L, 6586289336264687617L, 6617444900424221398L, + 8958380283753660417L, 5293955920339377119L, 1632681004890062849L, 8470329472543003390L, 6301638422566010881L, + 6776263578034402712L, 5041310738052808705L, 5421010862427522170L, 343699775700336641L, 8673617379884035472L, + 549919641120538625L, 6938893903907228377L, 5973958935009296385L, 5551115123125782702L, 1089818333265526785L, + 8881784197001252323L, 3588383740595798017L, 7105427357601001858L, 6560055807218548737L, 5684341886080801486L, + 8937393460516749313L, 9094947017729282379L, 1387108685230112769L, 7275957614183425903L, 2954361355555045377L, + 5820766091346740722L, 6052837899185946625L, 4656612873077392578L, 1152921504606846977L, 7450580596923828125L, 1L, + 5960464477539062500L, 1L, 4768371582031250000L, 1L, 7629394531250000000L, 1L, 6103515625000000000L, 1L, + 4882812500000000000L, 1L, 7812500000000000000L, 1L, 6250000000000000000L, 1L, 5000000000000000000L, 1L, + 8000000000000000000L, 1L, 6400000000000000000L, 1L, 5120000000000000000L, 1L, 8192000000000000000L, 1L, + 6553600000000000000L, 1L, 5242880000000000000L, 1L, 8388608000000000000L, 1L, 6710886400000000000L, 1L, + 5368709120000000000L, 1L, 8589934592000000000L, 1L, 6871947673600000000L, 1L, 5497558138880000000L, 1L, + 8796093022208000000L, 1L, 7036874417766400000L, 1L, 5629499534213120000L, 1L, 9007199254740992000L, 1L, + 7205759403792793600L, 1L, 5764607523034234880L, 1L, 4611686018427387904L, 1L, 7378697629483820646L, + 3689348814741910324L, 5902958103587056517L, 1106804644422573097L, 4722366482869645213L, 6419466937650923963L, + 7555786372591432341L, 8426472692870523179L, 6044629098073145873L, 4896503746925463381L, 4835703278458516698L, + 7606551812282281028L, 7737125245533626718L, 1102436455425918676L, 6189700196426901374L, 4571297979082645264L, + 4951760157141521099L, 5501712790637071373L, 7922816251426433759L, 3268717242906448711L, 6338253001141147007L, + 4459648201696114131L, 5070602400912917605L, 9101741783469756789L, 8112963841460668169L, 5339414816696835055L, + 6490371073168534535L, 6116206260728423206L, 5192296858534827628L, 4892965008582738565L, 8307674973655724205L, + 5984069606361426541L, 6646139978924579364L, 4787255685089141233L, 5316911983139663491L, 5674478955442268148L, + 8507059173023461586L, 5389817513965718714L, 6805647338418769269L, 2467179603801619810L, 5444517870735015415L, + 3818418090412251009L, 8711228593176024664L, 6109468944659601615L, 6968982874540819731L, 6732249563098636453L, + 5575186299632655785L, 3541125243107954001L, 8920298079412249256L, 5665800388972726402L, 7136238463529799405L, + 2687965903807225960L, 5708990770823839524L, 2150372723045780768L, 9134385233318143238L, 7129945171615159552L, + 7307508186654514591L, 169932915179262157L, 5846006549323611672L, 7514643961627230372L, 4676805239458889338L, + 2322366354559873974L, 7482888383134222941L, 1871111759924843197L, 5986310706507378352L, 8875587037423695204L, + 4789048565205902682L, 3411120815197045840L, 7662477704329444291L, 7302467711686228506L, 6129982163463555433L, + 3997299761978027643L, 4903985730770844346L, 6887188624324332438L, 7846377169233350954L, 7330152984177021577L, + 6277101735386680763L, 7708796794712572423L, 5021681388309344611L, 633014213657192454L, 8034690221294951377L, + 6546845963964373411L, 6427752177035961102L, 1548127956429588405L, 5142201741628768881L, 6772525587256536209L, + 8227522786606030210L, 7146692124868547611L, 6582018229284824168L, 5717353699894838089L, 5265614583427859334L, + 8263231774657780795L, 8424983333484574935L, 7687147617339583786L, 6739986666787659948L, 6149718093871667029L, + 5391989333430127958L, 8609123289839243947L, 8627182933488204734L, 2706550819517059345L, 6901746346790563787L, + 4009915062984602637L, 5521397077432451029L, 8741955272500547595L, 8834235323891921647L, 8453105213888010667L, + 7067388259113537318L, 3073135356368498210L, 5653910607290829854L, 6147857099836708891L, 9046256971665327767L, + 4302548137625868741L, 7237005577332262213L, 8976061732213560478L, 5789604461865809771L, 1646826163657982898L, + 4631683569492647816L, 8696158560410206965L, 7410693711188236507L, 1001132845059645012L, 5928554968950589205L, + 6334929498160581494L, 4742843975160471364L, 5067943598528465196L, 7588550360256754183L, 2574686535532678828L, + 6070840288205403346L, 5749098043168053386L, 4856672230564322677L, 2754604027163487547L, 7770675568902916283L, + 6252040850832535236L, 6216540455122333026L, 8690981495407938512L, 4973232364097866421L, 5108110788955395648L, + 7957171782556586274L, 4483628447586722714L, 6365737426045269019L, 5431577165440333333L, 5092589940836215215L, + 6189936139723221828L, 8148143905337944345L, 680525786702379117L, 6518515124270355476L, 544420629361903293L, + 5214812099416284380L, 7814234132973343281L, 8343699359066055009L, 3279402575902573442L, 6674959487252844007L, + 4468196468093013915L, 5339967589802275205L, 9108580396587276617L, 8543948143683640329L, 5350356597684866779L, + 6835158514946912263L, 6124959685518848585L, 5468126811957529810L, 8589316563156989191L, 8749002899132047697L, + 4519534464196406897L, 6999202319305638157L, 9149650793469991003L, 5599361855444510526L, 3630371820034082479L, + 8958978968711216842L, 2119246097312621643L, 7167183174968973473L, 7229420099962962799L, 5733746539975178779L, + 249512857857504755L, 9173994463960286046L, 4088569387313917931L, 7339195571168228837L, 1426181102480179183L, + 5871356456934583069L, 6674968104097008831L, 4697085165547666455L, 7184648890648562227L, 7515336264876266329L, + 2272066188182923754L, 6012269011901013063L, 3662327357917294165L, 4809815209520810450L, 6619210701075745655L, + 7695704335233296721L, 1367365084866417240L, 6156563468186637376L, 8472589697376954439L, 4925250774549309901L, + 4933397350530608390L, 7880401239278895842L, 4204086946107063100L, 6304320991423116673L, 8897292778998515965L, + 5043456793138493339L, 1583811001085947287L, 8069530869021589342L, 6223446416479425982L, 6455624695217271474L, + 1289408318441630463L, 5164499756173817179L, 2876201062124259532L, 8263199609878107486L, 8291270514140725574L, + 6610559687902485989L, 4788342003941625298L, 5288447750321988791L, 5675348010524255400L, 8461516400515182066L, + 5391208002096898316L, 6769213120412145653L, 2468291994306563491L, 5415370496329716522L, 5663982410187161116L, + 8664592794127546436L, 1683674226815637140L, 6931674235302037148L, 8725637010936330358L, 5545339388241629719L, + 1446486386636198802L, 8872543021186607550L, 6003727033359828406L, 7098034416949286040L, 4802981626687862725L, + 5678427533559428832L, 3842385301350290180L, 9085484053695086131L, 7992490889531419449L, 7268387242956068905L, + 4549318304254180398L, 5814709794364855124L, 3639454643403344318L, 4651767835491884099L, 4756238122093630616L, + 7442828536787014559L, 2075957773236943501L, 5954262829429611647L, 3505440625960509963L, 4763410263543689317L, + 8338375722881273455L, 7621456421669902908L, 5962703527126216881L, 6097165137335922326L, 8459511636442883828L, + 4877732109868737861L, 4922934901783351901L, 7804371375789980578L, 4187347028111452718L, 6243497100631984462L, + 7039226437231072498L, 4994797680505587570L, 1942032335042947675L, 7991676288808940112L, 3107251736068716280L, + 6393341031047152089L, 8019824610967838509L, 5114672824837721671L, 8260534096145225969L, 8183476519740354675L, + 304133702235675419L, 6546781215792283740L, 243306961788540335L, 5237424972633826992L, 194645569430832268L, + 8379879956214123187L, 2156107318460286790L, 6703903964971298549L, 7258909076881094917L, 5363123171977038839L, + 7651801668875831096L, 8580997075163262143L, 6708859448088464268L, 6864797660130609714L, 9056436373212681737L, + 5491838128104487771L, 9089823505941100552L, 8786941004967180435L, 1630996757909074751L, 7029552803973744348L, + 1304797406327259801L, 5623642243178995478L, 4733186739803718164L, 8997827589086392765L, 5728424376314993901L, + 7198262071269114212L, 4582739501051995121L, 5758609657015291369L, 9200214822954461581L, 9213775451224466191L, + 9186320494614273045L, 7371020360979572953L, 5504381988320463275L, 5896816288783658362L, 8092854405398280943L, + 4717453031026926690L, 2784934709576714431L, 7547924849643082704L, 4455895535322743090L, 6038339879714466163L, + 5409390835629149634L, 4830671903771572930L, 8016861483245230030L, 7729075046034516689L, 3603606336337592240L, + 6183260036827613351L, 4727559476441028954L, 4946608029462090681L, 1937373173781868001L, 7914572847139345089L, + 8633820300163854287L, 6331658277711476071L, 8751730647502038591L, 5065326622169180857L, 5156710110630675711L, + 8104522595470689372L, 872038547525260492L, 6483618076376551497L, 6231654060133073878L, 5186894461101241198L, + 1295974433364548779L, 8299031137761985917L, 228884686012322885L, 6639224910209588733L, 5717130970922723793L, + 5311379928167670986L, 8263053591480089358L, 8498207885068273579L, 308164894771456841L, 6798566308054618863L, + 2091206323188120634L, 5438853046443695090L, 5362313873292406831L, 8702164874309912144L, 8579702197267850929L, + 6961731899447929715L, 8708436165185235905L, 5569385519558343772L, 6966748932148188724L, 8911016831293350036L, + 3768100661953281312L, 7128813465034680029L, 1169806122191669888L, 5703050772027744023L, 2780519305124291072L, + 9124881235244390437L, 2604156480827910553L, 7299904988195512349L, 7617348406775193928L, 5839923990556409879L, + 7938553132791110304L, 4671939192445127903L, 8195516913603843405L, 7475102707912204646L, 2044780617540418478L, + 5980082166329763716L, 9014522123516155429L, 4784065733063810973L, 5366943291441969181L, 7654505172902097557L, + 6742434858936195528L, 6123604138321678046L, 1704599072407046100L, 4898883310657342436L, 8742376887409457526L, + 7838213297051747899L, 1075082168258445910L, 6270570637641398319L, 2704740141977711890L, 5016456510113118655L, + 4008466520953124674L, 8026330416180989848L, 6413546433524999478L, 6421064332944791878L, 8820185961561909905L, + 5136851466355833503L, 1522125547136662440L, 8218962346169333605L, 590726468047704741L, 6575169876935466884L, + 472581174438163793L, 5260135901548373507L, 2222739346921486196L, 8416217442477397611L, 5401057362445333075L, + 6732973953981918089L, 2476171482585311299L, 5386379163185534471L, 3825611593439204201L, 8618206661096855154L, + 2431629734760816398L, 6894565328877484123L, 3789978195179608280L, 5515652263101987298L, 6721331370885596947L, + 8825043620963179677L, 8909455786045999954L, 7060034896770543742L, 3438215814094889640L, 5648027917416434993L, + 8284595873388777197L, 9036844667866295990L, 2187306953196312545L, 7229475734293036792L, 1749845562557050036L, + 5783580587434429433L, 6933899672158505514L, 4626864469947543547L, 13096515613938926L, 7402983151916069675L, + 1865628832353257443L, 5922386521532855740L, 1492503065882605955L, 4737909217226284592L, 1194002452706084764L, + 7580654747562055347L, 3755078331700690783L, 6064523798049644277L, 8538085887473418112L, 4851619038439715422L, + 3141119895236824166L, 7762590461503544675L, 6870466239749873827L, 6210072369202835740L, 5496372991799899062L, + 4968057895362268592L, 4397098393439919250L, 7948892632579629747L, 8880031836874825961L, 6359114106063703798L, + 3414676654757950445L, 5087291284850963038L, 6421090138548270680L, 8139666055761540861L, 8429069814306277926L, + 6511732844609232689L, 4898581444074067179L, 5209386275687386151L, 5763539562630208905L, 8335018041099817842L, + 5532314485466423924L, 6668014432879854274L, 736502773631228816L, 5334411546303883419L, 2433876626275938215L, + 8535058474086213470L, 7583551416783411467L, 6828046779268970776L, 6066841133426729173L, 5462437423415176621L, + 3008798499370428177L, 8739899877464282594L, 1124728784250774760L, 6991919901971426075L, 2744457434771574970L, + 5593535921577140860L, 2195565947817259976L, 8949657474523425376L, 3512905516507615961L, 7159725979618740301L, + 965650005835137607L, 5727780783694992240L, 8151217634151930732L, 9164449253911987585L, 3818576177788313364L, + 7331559403129590068L, 3054860942230650691L, 5865247522503672054L, 6133237568526430876L, 4692198018002937643L, + 6751264462192099863L, 7507516828804700229L, 8957348732136404618L, 6006013463043760183L, 9010553393080078856L, + 4804810770435008147L, 1674419492351197600L, 7687697232696013035L, 4523745595132871322L, 6150157786156810428L, + 3618996476106297057L, 4920126228925448342L, 6584545995626947969L, 7872201966280717348L, 3156575963519296104L, + 6297761573024573878L, 6214609585557347207L, 5038209258419659102L, 8661036483187788089L, 8061134813471454564L, + 6478960743616640295L, 6448907850777163651L, 7027843002264267398L, 5159126280621730921L, 3777599994440458757L, + 8254602048994769474L, 2354811176362823687L, 6603681639195815579L, 3728523348461214111L, 5282945311356652463L, + 4827493086139926451L, 8452712498170643941L, 5879314530452927160L, 6762169998536515153L, 2858777216991386566L, + 5409735998829212122L, 5976370588335019576L, 8655577598126739396L, 2183495311852210675L, 6924462078501391516L, + 9125493878965589187L, 5539569662801113213L, 5455720695801516188L, 8863311460481781141L, 6884478705911470739L, + 7090649168385424913L, 3662908557358221429L, 5672519334708339930L, 6619675660628487467L, 9076030935533343889L, + 1368109020150804139L, 7260824748426675111L, 2939161623491598473L, 5808659798741340089L, 506654891422323617L, + 4646927838993072071L, 2249998320508814055L, 7435084542388915313L, 9134020534926967972L, 5948067633911132251L, + 1773193205828708893L, 4758454107128905800L, 8797252194146787761L, 7613526571406249281L, 4852231473780084609L, + 6090821257124999425L, 2037110771653112526L, 4872657005699999540L, 1629688617322490021L, 7796251209119999264L, + 2607501787715984033L, 6237000967295999411L, 3930675837543742388L, 4989600773836799529L, 1299866262664038749L, + 7983361238138879246L, 5769134835004372321L, 6386688990511103397L, 2770633460632542696L, 5109351192408882717L, + 7750529990618899641L, 8174961907854212348L, 5022150355506418780L, 6539969526283369878L, 7707069099147045347L, + 5231975621026695903L, 631632057204770793L, 8371160993642713444L, 8389308921011453915L, 6696928794914170755L, + 8556121544180118293L, 5357543035931336604L, 6844897235344094635L, 8572068857490138567L, 5417812354437685931L, + 6857655085992110854L, 644901068808238421L, 5486124068793688683L, 2360595262417545899L, 8777798510069901893L, + 1932278012497118276L, 7022238808055921514L, 5235171224739604944L, 5617791046444737211L, 6032811387162639117L, + 8988465674311579538L, 5963149404718312264L, 7190772539449263630L, 8459868338516560134L, 5752618031559410904L, + 6767894670813248108L, 9204188850495057447L, 5294608251188331487L + ) +} diff --git a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala index a14c51dc2..7b84a5b34 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala @@ -18,557 +18,6 @@ package zio.json.internal import java.io._ import scala.util.control.NoStackTrace -/** - * Total, fast, number parsing. - * - * The Java and Scala standard libraries throw exceptions when we attempt to parse an invalid number. Unfortunately, - * exceptions are very expensive, and untrusted data can be maliciously constructed to DOS a server. - * - * This suite of functions mitigates against such attacks by building up the numbers one character at a time, which has - * been shown through extensive benchmarking to be orders of magnitude faster than exception-throwing stdlib parsers, - * for valid and invalid inputs. This approach, proposed by alexknvl, was also benchmarked against regexp-based - * pre-validation. - * - * Note that although the behaviour is identical to the Java stdlib when given the canonical form of a primitive (i.e. - * the .toString) of a number there may be differences in behaviour for non-canonical forms. e.g. the Java stdlib may - * reject "1.0" when parsed as an `BigInteger` but we may parse it as a `1`, although "1.1" would be rejected. Parsing - * of `BigDecimal` preserves the trailing zeros on the right but not on the left, e.g. "000.00001000" will be - * "1.000e-5", which is useful in cases where the trailing zeros denote measurement accuracy. - * - * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit limit on the size of the significand, to - * avoid OOM style attacks, which is 128 bits by default. - * - * Results are contained in a specialisation of Option that avoids boxing. - */ -// TODO hex radix -// TODO octal radix -object SafeNumbers { - import UnsafeNumbers.UnsafeNumber - - def byte(num: String): ByteOption = - try ByteSome(UnsafeNumbers.byte(num)) - catch { case UnsafeNumber => ByteNone } - - def short(num: String): ShortOption = - try ShortSome(UnsafeNumbers.short(num)) - catch { case UnsafeNumber => ShortNone } - - def int(num: String): IntOption = - try IntSome(UnsafeNumbers.int(num)) - catch { case UnsafeNumber => IntNone } - - def long(num: String): LongOption = - try LongSome(UnsafeNumbers.long(num)) - catch { case UnsafeNumber => LongNone } - - def bigInteger( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigInteger] = - try Some(UnsafeNumbers.bigInteger(num, max_bits)) - catch { case UnsafeNumber => None } - - def float(num: String, max_bits: Int = 128): FloatOption = - try FloatSome(UnsafeNumbers.float(num, max_bits)) - catch { case UnsafeNumber => FloatNone } - - def double(num: String, max_bits: Int = 128): DoubleOption = - try DoubleSome(UnsafeNumbers.double(num, max_bits)) - catch { case UnsafeNumber => DoubleNone } - - def bigDecimal( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigDecimal] = - try Some(UnsafeNumbers.bigDecimal(num, max_bits)) - catch { case UnsafeNumber => None } - - // Based on the amazing work of Raffaello Giulietti - // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view - // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java - def toString(x: Double): String = { - val bits = java.lang.Double.doubleToLongBits(x) - val ieeeExponent = (bits >> 52).toInt & 0x7ff - val ieeeMantissa = bits & 0xfffffffffffffL - if (ieeeExponent == 2047) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" - } else { - val s = new java.lang.StringBuilder(24) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') - else { - var e = ieeeExponent - 1075 - var m = ieeeMantissa | 0x10000000000000L - var dv = 0L - var exp = 0 - if (e == 0) dv = m - else if (e >= -52 && e < 0 && m << e == 0) dv = m >> -e - else { - var expShift, expCorr = 0 - var cblShift = 2 - if (ieeeExponent == 0) { - e = -1074 - m = ieeeMantissa - if (ieeeMantissa < 3) { - m *= 10 - expShift = 1 - } - } else if (ieeeMantissa == 0 && ieeeExponent > 1) { - expCorr = 131007 - cblShift = 1 - } - exp = e * 315653 - expCorr >> 20 - val i = exp + 324 << 1 - val g1 = gs(i) - val g0 = gs(i + 1) - val h = (-exp * 108853 >> 15) + e + 2 - val cb = m << 2 - val outm1 = (m.toInt & 0x1) - 1 - val vb = rop(g1, g0, cb << h) - val vbls = rop(g1, g0, cb - cblShift << h) + outm1 - val vbrd = outm1 - rop(g1, g0, cb + 2 << h) - val s = vb >> 2 - if ( - s < 100 || { - dv = s / 10 // FIXME: Use Math.multiplyHigh(s, 1844674407370955168L) instead after dropping JDK 8 support - val sp40 = dv * 40 - val upin = (vbls - sp40).toInt - (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { - dv += ~upin >>> 31 - exp += 1 - false - } - } - ) { - val s4 = s << 2 - val uin = (vbls - s4).toInt - dv = (~ { - if ((((s4 + vbrd).toInt + 4) ^ uin) < 0) uin - else (vb.toInt & 0x3) + (s.toInt & 0x1) - 3 - } >>> 31) + s - exp -= expShift - } - } - val len = digitCount(dv) - exp += len - 1 - if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - s.append(dv) - var i = s.length - 1 - while (i > dotOff && s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s.insert(dotOff, '.').append('E').append(exp) - } else if (exp < 0) { - s.append('0').append('.') - while ({ - exp += 1 - exp != 0 - }) s.append('0') - s.append(dv) - var i = s.length - 1 - while (s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(dv) - var i = s.length - 1 - while (s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') - } - s.toString - } - } - - def toString(x: Float): String = { - val bits = java.lang.Float.floatToIntBits(x) - val ieeeExponent = (bits >> 23) & 0xff - val ieeeMantissa = bits & 0x7fffff - if (ieeeExponent == 255) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" - } else { - val s = new java.lang.StringBuilder(16) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') - else { - var e = ieeeExponent - 150 - var m = ieeeMantissa | 0x800000 - var dv, exp = 0 - if (e == 0) dv = m - else if (e >= -23 && e < 0 && m << e == 0) dv = m >> -e - else { - var expShift, expCorr = 0 - var cblShift = 2 - if (ieeeExponent == 0) { - e = -149 - m = ieeeMantissa - if (ieeeMantissa < 8) { - m *= 10 - expShift = 1 - } - } else if (ieeeMantissa == 0 && ieeeExponent > 1) { - expCorr = 131007 - cblShift = 1 - } - exp = e * 315653 - expCorr >> 20 - val g1 = gs(exp + 324 << 1) + 1 - val h = (-exp * 108853 >> 15) + e + 1 - val cb = m << 2 - val outm1 = (m & 0x1) - 1 - val vb = rop(g1, cb << h) - val vbls = rop(g1, cb - cblShift << h) + outm1 - val vbrd = outm1 - rop(g1, cb + 2 << h) - val s = vb >> 2 - if ( - s < 100 || { - dv = (s * 3435973837L >>> 35).toInt // divide a positive int by 10 - val sp40 = dv * 40 - val upin = vbls - sp40 - ((sp40 + vbrd + 40) ^ upin) >= 0 || { - dv += ~upin >>> 31 - exp += 1 - false - } - } - ) { - val s4 = s << 2 - val uin = vbls - s4 - dv = (~ { - if (((s4 + vbrd + 4) ^ uin) < 0) uin - else (vb & 0x3) + (s & 0x1) - 3 - } >>> 31) + s - exp -= expShift - } - } - val len = digitCount(dv.toLong) - exp += len - 1 - if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - s.append(dv) - var i = s.length - 1 - while (i > dotOff && s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s.insert(dotOff, '.').append('E').append(exp) - } else if (exp < 0) { - s.append('0').append('.') - while ({ - exp += 1 - exp != 0 - }) s.append('0') - s.append(dv) - var i = s.length - 1 - while (s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(dv) - var i = s.length - 1 - while (s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') - } - s.toString - } - } - - private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { - val x1 = multiplyHigh(g0, cp) // FIXME: Use Math.multiplyHigh after dropping JDK 8 support - val z = (g1 * cp >>> 1) + x1 - val y1 = multiplyHigh(g1, cp) // FIXME: Use Math.multiplyHigh after dropping JDK 8 support - (z >>> 63) + y1 | -(z & 0x7fffffffffffffffL) >>> 63 - } - - private[this] def rop(g: Long, cp: Int): Int = { - val x1 = - ((g & 0xffffffffL) * cp >>> 32) + (g >>> 32) * cp // FIXME: Use Math.multiplyHigh after dropping JDK 8 support - (x1 >>> 31).toInt | -x1.toInt >>> 31 - } - - private[this] def multiplyHigh(x: Long, y: Long): Long = { - val x2 = x & 0xffffffffL - val y2 = y & 0xffffffffL - val b = x2 * y2 - val x1 = x >>> 32 - val y1 = y >>> 32 - val a = x1 * y1 - (((b >>> 32) + (x1 + x2) * (y1 + y2) - b - a) >>> 32) + a - } - - // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: - // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ - private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt - - private[this] val offsets = Array( - 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, - 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 4889916394579099648L, 4889916394579099648L, - 4889916394579099648L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, - 4323355642275676160L, 4323355642275676160L, 4323355642275676160L, 4035215266123964416L, 4035215266123964416L, - 4035215266123964416L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, - 3458764413820540928L, 3458764413820540928L, 3458764413820540928L, 3170534127668829184L, 3170534127668829184L, - 3170534127668829184L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, - 2594073385265405696L, 2594073385265405696L, 2594073385265405696L, 2305843009203693952L, 2305843009203693952L, - 2305843009203693952L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, - 1729382256910170464L, 1729382256910170464L, 1729382256910170464L, 1441151880758548720L, 1441151880758548720L, - 1441151880758548720L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, - 864691128455135132L, 864691128455135132L, 864691128455135132L, 576460752303423478L, 576460752303423478L, - 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L - ) - - private[this] val gs: Array[Long] = Array( - 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, - 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, - 7466108948025751031L, 8492109508320019073L, 5972887158420600825L, 4949013199285060097L, 4778309726736480660L, - 3959210559428048077L, 7645295562778369056L, 6334736895084876923L, 6116236450222695245L, 3223115108696946377L, - 4892989160178156196L, 2578492086957557102L, 7828782656285049914L, 436238524390181040L, 6263026125028039931L, - 2193665226883099993L, 5010420900022431944L, 9133629810990300641L, 8016673440035891111L, 9079784475471615541L, - 6413338752028712889L, 5419153173006337271L, 5130671001622970311L, 6179996945776024979L, 8209073602596752498L, - 6198646298499729642L, 6567258882077401998L, 8648265853541694037L, 5253807105661921599L, 1384589460720489745L, - 8406091369059074558L, 5904691951894693915L, 6724873095247259646L, 8413102376257665455L, 5379898476197807717L, - 4885807493635177203L, 8607837561916492348L, 438594360332462878L, 6886270049533193878L, 4040224303007880625L, - 5509016039626555102L, 6921528257148214824L, 8814425663402488164L, 3695747581953323071L, 7051540530721990531L, - 4801272472933613619L, 5641232424577592425L, 1996343570975935733L, 9025971879324147880L, 3194149713561497173L, - 7220777503459318304L, 2555319770849197738L, 5776622002767454643L, 3888930224050313352L, 4621297602213963714L, - 6800492993982161005L, 7394076163542341943L, 5346765568258592123L, 5915260930833873554L, 7966761269348784022L, - 4732208744667098843L, 8218083422849982379L, 7571533991467358150L, 2080887032334240837L, 6057227193173886520L, - 1664709625867392670L, 4845781754539109216L, 1331767700693914136L, 7753250807262574745L, 7664851543223128102L, - 6202600645810059796L, 6131881234578502482L, 4962080516648047837L, 3060830580291846824L, 7939328826636876539L, - 6742003335837910079L, 6351463061309501231L, 7238277076041283225L, 5081170449047600985L, 3945947253462071419L, - 8129872718476161576L, 6313515605539314269L, 6503898174780929261L, 3206138077060496254L, 5203118539824743409L, - 720236054277441842L, 8324989663719589454L, 4841726501585817270L, 6659991730975671563L, 5718055608639608977L, - 5327993384780537250L, 8263793301653597505L, 8524789415648859601L, 3998697245790980200L, 6819831532519087681L, - 1354283389261828999L, 5455865226015270144L, 8462124340893283845L, 8729384361624432231L, 8005375723316388668L, - 6983507489299545785L, 4559626171282155773L, 5586805991439636628L, 3647700937025724618L, 8938889586303418605L, - 3991647091870204227L, 7151111669042734884L, 3193317673496163382L, 5720889335234187907L, 4399328546167885867L, - 9153422936374700651L, 8883600081239572549L, 7322738349099760521L, 5262205657620702877L, 5858190679279808417L, - 2365090118725607140L, 4686552543423846733L, 7426095317093351197L, 7498484069478154774L, 813706063123630946L, - 5998787255582523819L, 2495639257869859918L, 4799029804466019055L, 3841185813666843096L, 7678447687145630488L, - 6145897301866948954L, 6142758149716504390L, 8606066656235469486L, 4914206519773203512L, 6884853324988375589L, - 7862730431637125620L, 3637067690497580296L, 6290184345309700496L, 2909654152398064237L, 5032147476247760397L, - 483048914547496228L, 8051435961996416635L, 2617552670646949126L, 6441148769597133308L, 2094042136517559301L, - 5152919015677706646L, 5364582523955957764L, 8244670425084330634L, 4893983223587622099L, 6595736340067464507L, - 5759860986241052841L, 5276589072053971606L, 918539974250931950L, 8442542515286354569L, 7003687180914356604L, - 6754034012229083655L, 7447624152102440445L, 5403227209783266924L, 5958099321681952356L, 8645163535653227079L, - 3998935692578258285L, 6916130828522581663L, 5043822961433561789L, 5532904662818065330L, 7724407183888759755L, - 8852647460508904529L, 3135679457367239799L, 7082117968407123623L, 4353217973264747001L, 5665694374725698898L, - 7171923193353707924L, 9065110999561118238L, 407030665140201709L, 7252088799648894590L, 4014973346854071690L, - 5801671039719115672L, 3211978677483257352L, 4641336831775292537L, 8103606164099471367L, 7426138930840468060L, - 5587072233075333540L, 5940911144672374448L, 4469657786460266832L, 4752728915737899558L, 7265075043910123789L, - 7604366265180639294L, 556073626030467093L, 6083493012144511435L, 2289533308195328836L, 4866794409715609148L, - 1831626646556263069L, 7786871055544974637L, 1085928227119065748L, 6229496844435979709L, 6402765803808118083L, - 4983597475548783767L, 6966887050417449628L, 7973755960878054028L, 3768321651184098759L, 6379004768702443222L, - 6704006135689189330L, 5103203814961954578L, 1673856093809441141L, 8165126103939127325L, 833495342724150664L, - 6532100883151301860L, 666796274179320531L, 5225680706521041488L, 533437019343456425L, 8361089130433666380L, - 8232196860433350926L, 6688871304346933104L, 6585757488346680741L, 5351097043477546483L, 7113280398048299755L, - 8561755269564074374L, 313202192651548637L, 6849404215651259499L, 2095236161492194072L, 5479523372521007599L, - 3520863336564710419L, 8767237396033612159L, 99358116390671185L, 7013789916826889727L, 1924160900483492110L, - 5611031933461511781L, 7073351942499659173L, 8977651093538418850L, 7628014293257544353L, 7182120874830735080L, - 6102411434606035483L, 5745696699864588064L, 4881929147684828386L, 9193114719783340903L, 2277063414182859933L, - 7354491775826672722L, 5510999546088198270L, 5883593420661338178L, 719450822128648293L, 4706874736529070542L, - 4264909472444828957L, 7530999578446512867L, 8668529563282681493L, 6024799662757210294L, 3245474835884234871L, - 4819839730205768235L, 4441054276078343059L, 7711743568329229176L, 7105686841725348894L, 6169394854663383341L, - 3839875066009323953L, 4935515883730706673L, 1227225645436504001L, 7896825413969130677L, 118886625327451240L, - 6317460331175304541L, 5629132522374826477L, 5053968264940243633L, 2658631610528906020L, 8086349223904389813L, - 2409136169475294470L, 6469079379123511850L, 5616657750322145900L, 5175263503298809480L, 4493326200257716720L, - 8280421605278095168L, 7189321920412346751L, 6624337284222476135L, 217434314217011916L, 5299469827377980908L, - 173947451373609533L, 8479151723804769452L, 7657013551681595899L, 6783321379043815562L, 2436262026603366396L, - 5426657103235052449L, 7483032843395558602L, 8682651365176083919L, 6438829327320028278L, 6946121092140867135L, - 6995737869226977784L, 5556896873712693708L, 5596590295381582227L, 8891034997940309933L, 7109870065239576402L, - 7112827998352247947L, 153872830078795637L, 5690262398681798357L, 5657121486175901994L, 9104419837890877372L, - 1672696748397622544L, 7283535870312701897L, 6872180620830963520L, 5826828696250161518L, 1808395681922860493L, - 4661462957000129214L, 5136065360280198718L, 7458340731200206743L, 2683681354335452463L, 5966672584960165394L, - 5836293898210272294L, 4773338067968132315L, 6513709525939172997L, 7637340908749011705L, 1198563204647900987L, - 6109872726999209364L, 958850563718320789L, 4887898181599367491L, 2611754858345611793L, 7820637090558987986L, - 489458958611068546L, 6256509672447190388L, 7770264796372675483L, 5005207737957752311L, 682188614985274902L, - 8008332380732403697L, 6625525006089305327L, 6406665904585922958L, 1611071190129533939L, 5125332723668738366L, - 4978205766845537474L, 8200532357869981386L, 4275780412210949635L, 6560425886295985109L, 1575949922397804547L, - 5248340709036788087L, 3105434345289198799L, 8397345134458860939L, 6813369359833673240L, 6717876107567088751L, - 7295369895237893754L, 5374300886053671001L, 3991621508819359841L, 8598881417685873602L, 2697245599369065423L, - 6879105134148698881L, 7691819701608117823L, 5503284107318959105L, 4308781353915539097L, 8805254571710334568L, - 6894050166264862555L, 7044203657368267654L, 9204588947753800367L, 5635362925894614123L, 9208345565573995455L, - 9016580681431382598L, 3665306460692661759L, 7213264545145106078L, 6621593983296039730L, 5770611636116084862L, - 8986624001378742108L, 4616489308892867890L, 3499950386361083363L, 7386382894228588624L, 5599920618177733380L, - 5909106315382870899L, 6324610901913141866L, 4727285052306296719L, 6904363128901468655L, 7563656083690074751L, - 5512957784129484362L, 6050924866952059801L, 2565691819932632328L, 4840739893561647841L, 207879048575150701L, - 7745183829698636545L, 5866629699833106606L, 6196147063758909236L, 4693303759866485285L, 4956917651007127389L, - 1909968600522233067L, 7931068241611403822L, 6745298575577483229L, 6344854593289123058L, 1706890045720076260L, - 5075883674631298446L, 5054860851317971332L, 8121413879410077514L, 4398428547366843807L, 6497131103528062011L, - 5363417245264430207L, 5197704882822449609L, 2446059388840589004L, 8316327812515919374L, 7603043836886852730L, - 6653062250012735499L, 7927109476880437346L, 5322449800010188399L, 8186361988875305038L, 8515919680016301439L, - 7564155960087622576L, 6812735744013041151L, 7895999175441053223L, 5450188595210432921L, 4472124932981887417L, - 8720301752336692674L, 3466051078029109543L, 6976241401869354139L, 4617515269794242796L, 5580993121495483311L, - 5538686623206349399L, 8929588994392773298L, 5172549782388248714L, 7143671195514218638L, 7827388640652509295L, - 5714936956411374911L, 727887690409141951L, 9143899130258199857L, 6698643526767492606L, 7315119304206559886L, - 1669566006672083762L, 5852095443365247908L, 8714350434821487656L, 4681676354692198327L, 1437457125744324640L, - 7490682167507517323L, 4144605808561874585L, 5992545734006013858L, 7005033461591409992L, 4794036587204811087L, - 70003547160262509L, 7670458539527697739L, 1956680082827375175L, 6136366831622158191L, 3410018473632855302L, - 4909093465297726553L, 883340371535329080L, 7854549544476362484L, 8792042223940347174L, 6283639635581089987L, - 8878308186523232901L, 5026911708464871990L, 3413297734476675998L, 8043058733543795184L, 5461276375162681596L, - 6434446986835036147L, 6213695507501100438L, 5147557589468028918L, 1281607591258970028L, 8236092143148846269L, - 205897738643396882L, 6588873714519077015L, 2009392598285672668L, 5271098971615261612L, 1607514078628538134L, - 8433758354584418579L, 4416696933176616176L, 6747006683667534863L, 5378031953912248102L, 5397605346934027890L, - 7991774377871708805L, 8636168555094444625L, 3563466967739958280L, 6908934844075555700L, 2850773574191966624L, - 5527147875260444560L, 2280618859353573299L, 8843436600416711296L, 3648990174965717279L, 7074749280333369037L, - 1074517732601618662L, 5659799424266695229L, 6393637408194160414L, 9055679078826712367L, 4695796630997791177L, - 7244543263061369894L, 67288490056322619L, 5795634610449095915L, 1898505199416013257L, 4636507688359276732L, - 1518804159532810606L, 7418412301374842771L, 4274761062623452130L, 5934729841099874217L, 1575134442727806543L, - 4747783872879899373L, 6794130776295110719L, 7596454196607838997L, 9025934834701221989L, 6077163357286271198L, - 3531399053019067268L, 4861730685829016958L, 6514468057157164137L, 7778769097326427133L, 8578474484080507458L, - 6223015277861141707L, 1328756365151540482L, 4978412222288913365L, 6597028314234097870L, 7965459555662261385L, - 1331873265919780784L, 6372367644529809108L, 1065498612735824627L, 5097894115623847286L, 4541747704930570025L, - 8156630584998155658L, 3577447513147001717L, 6525304467998524526L, 6551306825259511697L, 5220243574398819621L, - 3396371052836654196L, 8352389719038111394L, 1744844869796736390L, 6681911775230489115L, 3240550303208344274L, - 5345529420184391292L, 2592440242566675419L, 8552847072295026067L, 5992578795477635832L, 6842277657836020854L, - 1104714221640198342L, 5473822126268816683L, 2728445784683113836L, 8758115402030106693L, 2520838848122026975L, - 7006492321624085354L, 5706019893239531903L, 5605193857299268283L, 6409490321962580684L, 8968310171678829253L, - 8410510107769173933L, 7174648137343063403L, 1194384864102473662L, 5739718509874450722L, 4644856706023889253L, - 9183549615799121156L, 53073100154402158L, 7346839692639296924L, 7421156109607342373L, 5877471754111437539L, - 7781599295056829060L, 4701977403289150031L, 8069953843416418410L, 7523163845262640050L, 9222577334724359132L, - 6018531076210112040L, 7378061867779487306L, 4814824860968089632L, 5902449494223589845L, 7703719777548943412L, - 2065221561273923105L, 6162975822039154729L, 7186200471132003969L, 4930380657631323783L, 7593634784276558337L, - 7888609052210118054L, 1081769210616762369L, 6310887241768094443L, 2710089775864365057L, 5048709793414475554L, - 5857420635433402369L, 8077935669463160887L, 3837849794580578305L, 6462348535570528709L, 8604303057777328129L, - 5169878828456422967L, 8728116853592817665L, 8271806125530276748L, 6586289336264687617L, 6617444900424221398L, - 8958380283753660417L, 5293955920339377119L, 1632681004890062849L, 8470329472543003390L, 6301638422566010881L, - 6776263578034402712L, 5041310738052808705L, 5421010862427522170L, 343699775700336641L, 8673617379884035472L, - 549919641120538625L, 6938893903907228377L, 5973958935009296385L, 5551115123125782702L, 1089818333265526785L, - 8881784197001252323L, 3588383740595798017L, 7105427357601001858L, 6560055807218548737L, 5684341886080801486L, - 8937393460516749313L, 9094947017729282379L, 1387108685230112769L, 7275957614183425903L, 2954361355555045377L, - 5820766091346740722L, 6052837899185946625L, 4656612873077392578L, 1152921504606846977L, 7450580596923828125L, 1L, - 5960464477539062500L, 1L, 4768371582031250000L, 1L, 7629394531250000000L, 1L, 6103515625000000000L, 1L, - 4882812500000000000L, 1L, 7812500000000000000L, 1L, 6250000000000000000L, 1L, 5000000000000000000L, 1L, - 8000000000000000000L, 1L, 6400000000000000000L, 1L, 5120000000000000000L, 1L, 8192000000000000000L, 1L, - 6553600000000000000L, 1L, 5242880000000000000L, 1L, 8388608000000000000L, 1L, 6710886400000000000L, 1L, - 5368709120000000000L, 1L, 8589934592000000000L, 1L, 6871947673600000000L, 1L, 5497558138880000000L, 1L, - 8796093022208000000L, 1L, 7036874417766400000L, 1L, 5629499534213120000L, 1L, 9007199254740992000L, 1L, - 7205759403792793600L, 1L, 5764607523034234880L, 1L, 4611686018427387904L, 1L, 7378697629483820646L, - 3689348814741910324L, 5902958103587056517L, 1106804644422573097L, 4722366482869645213L, 6419466937650923963L, - 7555786372591432341L, 8426472692870523179L, 6044629098073145873L, 4896503746925463381L, 4835703278458516698L, - 7606551812282281028L, 7737125245533626718L, 1102436455425918676L, 6189700196426901374L, 4571297979082645264L, - 4951760157141521099L, 5501712790637071373L, 7922816251426433759L, 3268717242906448711L, 6338253001141147007L, - 4459648201696114131L, 5070602400912917605L, 9101741783469756789L, 8112963841460668169L, 5339414816696835055L, - 6490371073168534535L, 6116206260728423206L, 5192296858534827628L, 4892965008582738565L, 8307674973655724205L, - 5984069606361426541L, 6646139978924579364L, 4787255685089141233L, 5316911983139663491L, 5674478955442268148L, - 8507059173023461586L, 5389817513965718714L, 6805647338418769269L, 2467179603801619810L, 5444517870735015415L, - 3818418090412251009L, 8711228593176024664L, 6109468944659601615L, 6968982874540819731L, 6732249563098636453L, - 5575186299632655785L, 3541125243107954001L, 8920298079412249256L, 5665800388972726402L, 7136238463529799405L, - 2687965903807225960L, 5708990770823839524L, 2150372723045780768L, 9134385233318143238L, 7129945171615159552L, - 7307508186654514591L, 169932915179262157L, 5846006549323611672L, 7514643961627230372L, 4676805239458889338L, - 2322366354559873974L, 7482888383134222941L, 1871111759924843197L, 5986310706507378352L, 8875587037423695204L, - 4789048565205902682L, 3411120815197045840L, 7662477704329444291L, 7302467711686228506L, 6129982163463555433L, - 3997299761978027643L, 4903985730770844346L, 6887188624324332438L, 7846377169233350954L, 7330152984177021577L, - 6277101735386680763L, 7708796794712572423L, 5021681388309344611L, 633014213657192454L, 8034690221294951377L, - 6546845963964373411L, 6427752177035961102L, 1548127956429588405L, 5142201741628768881L, 6772525587256536209L, - 8227522786606030210L, 7146692124868547611L, 6582018229284824168L, 5717353699894838089L, 5265614583427859334L, - 8263231774657780795L, 8424983333484574935L, 7687147617339583786L, 6739986666787659948L, 6149718093871667029L, - 5391989333430127958L, 8609123289839243947L, 8627182933488204734L, 2706550819517059345L, 6901746346790563787L, - 4009915062984602637L, 5521397077432451029L, 8741955272500547595L, 8834235323891921647L, 8453105213888010667L, - 7067388259113537318L, 3073135356368498210L, 5653910607290829854L, 6147857099836708891L, 9046256971665327767L, - 4302548137625868741L, 7237005577332262213L, 8976061732213560478L, 5789604461865809771L, 1646826163657982898L, - 4631683569492647816L, 8696158560410206965L, 7410693711188236507L, 1001132845059645012L, 5928554968950589205L, - 6334929498160581494L, 4742843975160471364L, 5067943598528465196L, 7588550360256754183L, 2574686535532678828L, - 6070840288205403346L, 5749098043168053386L, 4856672230564322677L, 2754604027163487547L, 7770675568902916283L, - 6252040850832535236L, 6216540455122333026L, 8690981495407938512L, 4973232364097866421L, 5108110788955395648L, - 7957171782556586274L, 4483628447586722714L, 6365737426045269019L, 5431577165440333333L, 5092589940836215215L, - 6189936139723221828L, 8148143905337944345L, 680525786702379117L, 6518515124270355476L, 544420629361903293L, - 5214812099416284380L, 7814234132973343281L, 8343699359066055009L, 3279402575902573442L, 6674959487252844007L, - 4468196468093013915L, 5339967589802275205L, 9108580396587276617L, 8543948143683640329L, 5350356597684866779L, - 6835158514946912263L, 6124959685518848585L, 5468126811957529810L, 8589316563156989191L, 8749002899132047697L, - 4519534464196406897L, 6999202319305638157L, 9149650793469991003L, 5599361855444510526L, 3630371820034082479L, - 8958978968711216842L, 2119246097312621643L, 7167183174968973473L, 7229420099962962799L, 5733746539975178779L, - 249512857857504755L, 9173994463960286046L, 4088569387313917931L, 7339195571168228837L, 1426181102480179183L, - 5871356456934583069L, 6674968104097008831L, 4697085165547666455L, 7184648890648562227L, 7515336264876266329L, - 2272066188182923754L, 6012269011901013063L, 3662327357917294165L, 4809815209520810450L, 6619210701075745655L, - 7695704335233296721L, 1367365084866417240L, 6156563468186637376L, 8472589697376954439L, 4925250774549309901L, - 4933397350530608390L, 7880401239278895842L, 4204086946107063100L, 6304320991423116673L, 8897292778998515965L, - 5043456793138493339L, 1583811001085947287L, 8069530869021589342L, 6223446416479425982L, 6455624695217271474L, - 1289408318441630463L, 5164499756173817179L, 2876201062124259532L, 8263199609878107486L, 8291270514140725574L, - 6610559687902485989L, 4788342003941625298L, 5288447750321988791L, 5675348010524255400L, 8461516400515182066L, - 5391208002096898316L, 6769213120412145653L, 2468291994306563491L, 5415370496329716522L, 5663982410187161116L, - 8664592794127546436L, 1683674226815637140L, 6931674235302037148L, 8725637010936330358L, 5545339388241629719L, - 1446486386636198802L, 8872543021186607550L, 6003727033359828406L, 7098034416949286040L, 4802981626687862725L, - 5678427533559428832L, 3842385301350290180L, 9085484053695086131L, 7992490889531419449L, 7268387242956068905L, - 4549318304254180398L, 5814709794364855124L, 3639454643403344318L, 4651767835491884099L, 4756238122093630616L, - 7442828536787014559L, 2075957773236943501L, 5954262829429611647L, 3505440625960509963L, 4763410263543689317L, - 8338375722881273455L, 7621456421669902908L, 5962703527126216881L, 6097165137335922326L, 8459511636442883828L, - 4877732109868737861L, 4922934901783351901L, 7804371375789980578L, 4187347028111452718L, 6243497100631984462L, - 7039226437231072498L, 4994797680505587570L, 1942032335042947675L, 7991676288808940112L, 3107251736068716280L, - 6393341031047152089L, 8019824610967838509L, 5114672824837721671L, 8260534096145225969L, 8183476519740354675L, - 304133702235675419L, 6546781215792283740L, 243306961788540335L, 5237424972633826992L, 194645569430832268L, - 8379879956214123187L, 2156107318460286790L, 6703903964971298549L, 7258909076881094917L, 5363123171977038839L, - 7651801668875831096L, 8580997075163262143L, 6708859448088464268L, 6864797660130609714L, 9056436373212681737L, - 5491838128104487771L, 9089823505941100552L, 8786941004967180435L, 1630996757909074751L, 7029552803973744348L, - 1304797406327259801L, 5623642243178995478L, 4733186739803718164L, 8997827589086392765L, 5728424376314993901L, - 7198262071269114212L, 4582739501051995121L, 5758609657015291369L, 9200214822954461581L, 9213775451224466191L, - 9186320494614273045L, 7371020360979572953L, 5504381988320463275L, 5896816288783658362L, 8092854405398280943L, - 4717453031026926690L, 2784934709576714431L, 7547924849643082704L, 4455895535322743090L, 6038339879714466163L, - 5409390835629149634L, 4830671903771572930L, 8016861483245230030L, 7729075046034516689L, 3603606336337592240L, - 6183260036827613351L, 4727559476441028954L, 4946608029462090681L, 1937373173781868001L, 7914572847139345089L, - 8633820300163854287L, 6331658277711476071L, 8751730647502038591L, 5065326622169180857L, 5156710110630675711L, - 8104522595470689372L, 872038547525260492L, 6483618076376551497L, 6231654060133073878L, 5186894461101241198L, - 1295974433364548779L, 8299031137761985917L, 228884686012322885L, 6639224910209588733L, 5717130970922723793L, - 5311379928167670986L, 8263053591480089358L, 8498207885068273579L, 308164894771456841L, 6798566308054618863L, - 2091206323188120634L, 5438853046443695090L, 5362313873292406831L, 8702164874309912144L, 8579702197267850929L, - 6961731899447929715L, 8708436165185235905L, 5569385519558343772L, 6966748932148188724L, 8911016831293350036L, - 3768100661953281312L, 7128813465034680029L, 1169806122191669888L, 5703050772027744023L, 2780519305124291072L, - 9124881235244390437L, 2604156480827910553L, 7299904988195512349L, 7617348406775193928L, 5839923990556409879L, - 7938553132791110304L, 4671939192445127903L, 8195516913603843405L, 7475102707912204646L, 2044780617540418478L, - 5980082166329763716L, 9014522123516155429L, 4784065733063810973L, 5366943291441969181L, 7654505172902097557L, - 6742434858936195528L, 6123604138321678046L, 1704599072407046100L, 4898883310657342436L, 8742376887409457526L, - 7838213297051747899L, 1075082168258445910L, 6270570637641398319L, 2704740141977711890L, 5016456510113118655L, - 4008466520953124674L, 8026330416180989848L, 6413546433524999478L, 6421064332944791878L, 8820185961561909905L, - 5136851466355833503L, 1522125547136662440L, 8218962346169333605L, 590726468047704741L, 6575169876935466884L, - 472581174438163793L, 5260135901548373507L, 2222739346921486196L, 8416217442477397611L, 5401057362445333075L, - 6732973953981918089L, 2476171482585311299L, 5386379163185534471L, 3825611593439204201L, 8618206661096855154L, - 2431629734760816398L, 6894565328877484123L, 3789978195179608280L, 5515652263101987298L, 6721331370885596947L, - 8825043620963179677L, 8909455786045999954L, 7060034896770543742L, 3438215814094889640L, 5648027917416434993L, - 8284595873388777197L, 9036844667866295990L, 2187306953196312545L, 7229475734293036792L, 1749845562557050036L, - 5783580587434429433L, 6933899672158505514L, 4626864469947543547L, 13096515613938926L, 7402983151916069675L, - 1865628832353257443L, 5922386521532855740L, 1492503065882605955L, 4737909217226284592L, 1194002452706084764L, - 7580654747562055347L, 3755078331700690783L, 6064523798049644277L, 8538085887473418112L, 4851619038439715422L, - 3141119895236824166L, 7762590461503544675L, 6870466239749873827L, 6210072369202835740L, 5496372991799899062L, - 4968057895362268592L, 4397098393439919250L, 7948892632579629747L, 8880031836874825961L, 6359114106063703798L, - 3414676654757950445L, 5087291284850963038L, 6421090138548270680L, 8139666055761540861L, 8429069814306277926L, - 6511732844609232689L, 4898581444074067179L, 5209386275687386151L, 5763539562630208905L, 8335018041099817842L, - 5532314485466423924L, 6668014432879854274L, 736502773631228816L, 5334411546303883419L, 2433876626275938215L, - 8535058474086213470L, 7583551416783411467L, 6828046779268970776L, 6066841133426729173L, 5462437423415176621L, - 3008798499370428177L, 8739899877464282594L, 1124728784250774760L, 6991919901971426075L, 2744457434771574970L, - 5593535921577140860L, 2195565947817259976L, 8949657474523425376L, 3512905516507615961L, 7159725979618740301L, - 965650005835137607L, 5727780783694992240L, 8151217634151930732L, 9164449253911987585L, 3818576177788313364L, - 7331559403129590068L, 3054860942230650691L, 5865247522503672054L, 6133237568526430876L, 4692198018002937643L, - 6751264462192099863L, 7507516828804700229L, 8957348732136404618L, 6006013463043760183L, 9010553393080078856L, - 4804810770435008147L, 1674419492351197600L, 7687697232696013035L, 4523745595132871322L, 6150157786156810428L, - 3618996476106297057L, 4920126228925448342L, 6584545995626947969L, 7872201966280717348L, 3156575963519296104L, - 6297761573024573878L, 6214609585557347207L, 5038209258419659102L, 8661036483187788089L, 8061134813471454564L, - 6478960743616640295L, 6448907850777163651L, 7027843002264267398L, 5159126280621730921L, 3777599994440458757L, - 8254602048994769474L, 2354811176362823687L, 6603681639195815579L, 3728523348461214111L, 5282945311356652463L, - 4827493086139926451L, 8452712498170643941L, 5879314530452927160L, 6762169998536515153L, 2858777216991386566L, - 5409735998829212122L, 5976370588335019576L, 8655577598126739396L, 2183495311852210675L, 6924462078501391516L, - 9125493878965589187L, 5539569662801113213L, 5455720695801516188L, 8863311460481781141L, 6884478705911470739L, - 7090649168385424913L, 3662908557358221429L, 5672519334708339930L, 6619675660628487467L, 9076030935533343889L, - 1368109020150804139L, 7260824748426675111L, 2939161623491598473L, 5808659798741340089L, 506654891422323617L, - 4646927838993072071L, 2249998320508814055L, 7435084542388915313L, 9134020534926967972L, 5948067633911132251L, - 1773193205828708893L, 4758454107128905800L, 8797252194146787761L, 7613526571406249281L, 4852231473780084609L, - 6090821257124999425L, 2037110771653112526L, 4872657005699999540L, 1629688617322490021L, 7796251209119999264L, - 2607501787715984033L, 6237000967295999411L, 3930675837543742388L, 4989600773836799529L, 1299866262664038749L, - 7983361238138879246L, 5769134835004372321L, 6386688990511103397L, 2770633460632542696L, 5109351192408882717L, - 7750529990618899641L, 8174961907854212348L, 5022150355506418780L, 6539969526283369878L, 7707069099147045347L, - 5231975621026695903L, 631632057204770793L, 8371160993642713444L, 8389308921011453915L, 6696928794914170755L, - 8556121544180118293L, 5357543035931336604L, 6844897235344094635L, 8572068857490138567L, 5417812354437685931L, - 6857655085992110854L, 644901068808238421L, 5486124068793688683L, 2360595262417545899L, 8777798510069901893L, - 1932278012497118276L, 7022238808055921514L, 5235171224739604944L, 5617791046444737211L, 6032811387162639117L, - 8988465674311579538L, 5963149404718312264L, 7190772539449263630L, 8459868338516560134L, 5752618031559410904L, - 6767894670813248108L, 9204188850495057447L, 5294608251188331487L - ) -} - // specialised Options to avoid boxing. Prefer .isEmpty guarded access to .value // for higher performance: pattern matching is slightly slower. From bb1e1d12725360cbfa4eaa98525bcf30e42d8188 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 27 Jan 2025 21:40:08 +0100 Subject: [PATCH 114/311] Add duplication check for ADT case names (#1259) --- .../src/main/scala-2.x/zio/json/macros.scala | 7 +++++++ .../shared/src/main/scala-3/zio/json/macros.scala | 5 +++++ .../src/test/scala/zio/json/DecoderSpec.scala | 14 ++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 3b9e62edf..369bb1e89 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -398,6 +398,13 @@ object DeriveJsonDecoder { val names = ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeName.short)) }.toArray + if (names.distinct.length != names.length) { + val collisions = names.groupBy(identity).collect { case (n, ns) if ns.lengthCompare(1) > 0 => n } + throw new AssertionError( + s"Case names in ADT ${ctx.typeName.full} must be distinct, " + + s"name(s) ${collisions.mkString(",")} are duplicated" + ) + } val matrix = new StringMatrix(names) lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap = names.zipWithIndex.toMap diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 276e5c36a..99f857f77 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -404,6 +404,11 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv val names: Array[String] = IArray.genericWrapArray(ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeInfo.short)) }).toArray + if (names.distinct.length != names.length) { + val collisions = names.groupBy(identity).collect { case (n, ns) if ns.lengthCompare(1) > 0 => n } + throw new AssertionError(s"Case names in ADT ${ctx.typeInfo.full} must be distinct, " + + s"name(s) ${collisions.mkString(",")} are duplicated") + } val matrix: StringMatrix = new StringMatrix(names) lazy val tcs: Array[JsonDecoder[Any]] = IArray.genericWrapArray(ctx.subtypes.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 37be1314d..7afaeaa80 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -282,6 +282,20 @@ object DecoderSpec extends ZIOSpecDefault { assert("""{"hint":"Child2"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && assert("""{"child1":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) }, + test("sum with duplicated case names") { + for { + error <- ZIO.attempt { + sealed trait Fruit + case class Banana(curvature: Double) extends Fruit + @jsonHint("Banana") case class Apple(color: String) extends Fruit + DeriveJsonDecoder.gen[Fruit] + }.flip + } yield assertTrue( + error.getMessage.matches( + """Case names in ADT zio.json.DecoderSpec.spec(.\$anonfun)?.Fruit must be distinct, name\(s\) Banana are duplicated""" + ) + ) + }, test("unicode") { assert(""""€🐵🥰"""".fromJson[String])(isRight(equalTo("€🐵🥰"))) }, From 796ad55ca4a53c2d286412de1d1a1bb8052cda6b Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 29 Jan 2025 16:53:26 +0100 Subject: [PATCH 115/311] More efficient encoding of case classes (#1262) --- .../src/main/scala-2.x/zio/json/macros.scala | 18 ++- .../src/main/scala-3/zio/json/macros.scala | 14 ++- .../zio/json/internal/FieldEncoder.scala | 64 +++++----- .../internal/FieldEncoderHelperSpec.scala | 113 ++---------------- 4 files changed, 65 insertions(+), 144 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 369bb1e89..9c2dd8a50 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -524,7 +524,7 @@ object DeriveJsonEncoder { val withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.encoding }.getOrElse(explicitEmptyCollections) - new FieldEncoder( + FieldEncoder( p, name, p.typeclass.asInstanceOf[JsonEncoder[Any]], @@ -546,9 +546,17 @@ object DeriveJsonEncoder { var idx = 0 var prevFields = false // whether any fields have been written while (idx < fields.length) { - val field = fields(idx) - val p = field.p.dereference(a) - field.encodeOrSkip(p) { () => + val field = fields(idx) + val p = field.p.dereference(a) + val encoder = field.encoder + if ({ + (field.flags: @switch) match { + case 0 => !encoder.isEmpty(p) && !encoder.isNothing(p) + case 1 => !encoder.isNothing(p) + case 2 => !encoder.isEmpty(p) + case _ => true + } + }) { // if we have at least one field already, we need a comma if (prevFields) { out.write(',') @@ -557,7 +565,7 @@ object DeriveJsonEncoder { JsonEncoder.string.unsafeEncode(field.name, indent_, out) if (indent.isEmpty) out.write(':') else out.write(" : ") - field.encoder.unsafeEncode(p, indent_, out) + encoder.unsafeEncode(p, indent_, out) prevFields = true // record that we have at least one field so far } idx += 1 diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 99f857f77..dd619e6da 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -577,7 +577,7 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv val withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.encoding }.getOrElse(explicitEmptyCollections) - new FieldEncoder( + FieldEncoder( p, name, p.typeclass.asInstanceOf[JsonEncoder[Any]], @@ -601,7 +601,15 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv while (idx < fields.length) { val field = fields(idx) val p = field.p.deref(a) - field.encodeOrSkip(p) { () => + val encoder = field.encoder + if ({ + (field.flags: @switch) match { + case 0 => !encoder.isEmpty(p) && !encoder.isNothing(p) + case 1 => !encoder.isNothing(p) + case 2 => !encoder.isEmpty(p) + case _ => true + } + }) { // if we have at least one field already, we need a comma if (prevFields) { out.write(',') @@ -610,7 +618,7 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv JsonEncoder.string.unsafeEncode(field.name, indent_, out) if (indent.isEmpty) out.write(':') else out.write(" : ") - field.encoder.unsafeEncode(p, indent_, out) + encoder.unsafeEncode(p, indent_, out) prevFields = true // at least one field so far } idx += 1 diff --git a/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala b/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala index ac2cbc34f..73dc5034e 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala @@ -1,44 +1,48 @@ -package zio.json -package internal +package zio.json.internal import zio.Chunk +import zio.json._ import zio.json.ast.Json +import scala.annotation.switch private[json] class FieldEncoder[T, P]( val p: P, val name: String, val encoder: JsonEncoder[T], - withExplicitNulls: Boolean, - withExplicitEmptyCollections: Boolean + val flags: Int ) { - private[this] val _encodeOrSkip: T => (() => Unit) => Unit = - if (withExplicitNulls && withExplicitEmptyCollections) { _ => encode => - encode() - } else if (withExplicitNulls) { t => encode => - if (!encoder.isEmpty(t)) encode() else () - } else if (withExplicitEmptyCollections) { t => encode => - if (!encoder.isNothing(t)) encode() else () - } else { t => encode => - if (!encoder.isEmpty(t) && !encoder.isNothing(t)) encode() else () - } - def encodeOrSkip(t: T)(encode: () => Unit): Unit = _encodeOrSkip(t)(encode) - - private[this] val _encodeOrDefault: T => ( - Either[String, Chunk[(String, Json)]], - () => Either[String, Chunk[(String, Json)]] - ) => Either[String, Chunk[(String, Json)]] = - if (withExplicitNulls && withExplicitEmptyCollections) { _ => (_, encode) => - encode() - } else if (withExplicitNulls) { t => (default, encode) => - if (!encoder.isEmpty(t)) encode() else default - } else if (withExplicitEmptyCollections) { t => (default, encode) => - if (!encoder.isNothing(t)) encode() else default - } else { t => (default, encode) => - if (!encoder.isEmpty(t) && !encoder.isNothing(t)) encode() else default - } def encodeOrDefault(t: T)( encode: () => Either[String, Chunk[(String, Json)]], default: Either[String, Chunk[(String, Json)]] ): Either[String, Chunk[(String, Json)]] = - _encodeOrDefault(t)(default, encode) + (flags: @switch) match { + case 0 => + if (!encoder.isEmpty(t) && !encoder.isNothing(t)) encode() else default + case 1 => + if (!encoder.isNothing(t)) encode() else default + case 2 => + if (!encoder.isEmpty(t)) encode() else default + case _ => + encode() + } +} + +private[json] object FieldEncoder { + def apply[T, P]( + p: P, + name: String, + encoder: JsonEncoder[T], + withExplicitNulls: Boolean, + withExplicitEmptyCollections: Boolean + ): FieldEncoder[T, P] = + new FieldEncoder( + p, + name, + encoder, { + if (withExplicitNulls) { + if (withExplicitEmptyCollections) 3 else 2 + } else if (withExplicitEmptyCollections) 1 + else 0 + } + ) } diff --git a/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala index 6d5fb6c6c..a18b61d1d 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala @@ -7,109 +7,10 @@ import zio.test._ object FieldEncoderSpec extends ZIOSpecDefault { val spec = suite("FieldEncoder")( - suite("encodeOrSkip")( - suite("OptionEncoder")( - test("should skip encoding None when withExplicitNulls is false") { - val helper = new FieldEncoder( - 1, - "test", - JsonEncoder.option(JsonEncoder.int), - withExplicitNulls = false, - withExplicitEmptyCollections = false - ) - var called = false - helper.encodeOrSkip(None)(() => called = true) - assertTrue(!called) - }, - test("should encode None when withExplicitNulls is true") { - val helper = new FieldEncoder( - 1, - "test", - JsonEncoder.option(JsonEncoder.int), - withExplicitNulls = true, - withExplicitEmptyCollections = false - ) - var called = false - helper.encodeOrSkip(None)(() => called = true) - assertTrue(called) - } - ), - suite("CollectionEncoder")( - suite("for a List")( - test("should encode empty collections when withExplicitEmptyCollections is true") { - val helper = new FieldEncoder( - 1, - "test", - implicitly[JsonEncoder[List[Int]]], - withExplicitNulls = false, - withExplicitEmptyCollections = true - ) - var called = false - helper.encodeOrSkip(Nil)(() => called = true) - assertTrue(called) - }, - test("should not encode empty collections when withExplicitEmptyCollections is false") { - val helper = new FieldEncoder( - 1, - "test", - implicitly[JsonEncoder[List[Int]]], - withExplicitNulls = false, - withExplicitEmptyCollections = false - ) - var called = false - helper.encodeOrSkip(Nil)(() => called = true) - assertTrue(!called) - } - ), - suite("for a case class")( - test("should encode case classes with empty collections when withExplicitEmptyCollections is true") { - case class Test(list: List[Int], option: Option[Int]) - val helper = new FieldEncoder( - 1, - "test", - DeriveJsonEncoder.gen[Test], - withExplicitNulls = false, - withExplicitEmptyCollections = true - ) - var called = false - helper.encodeOrSkip(Test(Nil, None))(() => called = true) - assertTrue(called) - }, - test("should not encode case classes with empty collections when withExplicitEmptyCollections is false") { - case class Test(list: List[Int], option: Option[Int]) - val helper = new FieldEncoder( - 1, - "test", - DeriveJsonEncoder.gen[Test], - withExplicitNulls = false, - withExplicitEmptyCollections = false - ) - var called = false - helper.encodeOrSkip(Test(Nil, None))(() => called = true) - assertTrue(!called) - }, - test( - "should also not encode case classes with empty options when withExplicitEmptyCollections is false, even when withExplicitNulls is true" - ) { - case class Test(list: List[Int], option: Option[Int]) - val helper = new FieldEncoder( - 1, - "test", - DeriveJsonEncoder.gen[Test], - withExplicitNulls = true, - withExplicitEmptyCollections = false - ) - var called = false - helper.encodeOrSkip(Test(Nil, None))(() => called = true) - assertTrue(!called) - } - ) - ) - ), suite("encodeOrDefault")( suite("OptionEncoder")( test("should use the default encoding None when withExplicitNulls is false") { - val helper = new FieldEncoder( + val helper = FieldEncoder( 1, "test", JsonEncoder.option(JsonEncoder.int), @@ -122,7 +23,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { ) }, test("should encode None when withExplicitNulls is true") { - val helper = new FieldEncoder( + val helper = FieldEncoder( 1, "test", JsonEncoder.option(JsonEncoder.int), @@ -137,7 +38,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { ), suite("CollectionEncoder")( test("should encode empty collections when withExplicitEmptyCollections is true") { - val helper = new FieldEncoder( + val helper = FieldEncoder( 1, "test", implicitly[JsonEncoder[List[Int]]], @@ -150,7 +51,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { ) }, test("should not encode empty collections when withExplicitEmptyCollections is false") { - val helper = new FieldEncoder( + val helper = FieldEncoder( 1, "test", implicitly[JsonEncoder[List[Int]]], @@ -166,7 +67,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { suite("for a case class")( test("should encode case classes with empty collections when withExplicitEmptyCollections is true") { case class Test(list: List[Int], option: Option[Int]) - val helper = new FieldEncoder( + val helper = FieldEncoder( 1, "test", DeriveJsonEncoder.gen[Test], @@ -183,7 +84,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { }, test("should not encode case classes with empty collections when withExplicitEmptyCollections is false") { case class Test(list: List[Int], option: Option[Int]) - val helper = new FieldEncoder( + val helper = FieldEncoder( 1, "test", DeriveJsonEncoder.gen[Test], @@ -199,7 +100,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { "should also not encode case classes with empty options when withExplicitEmptyCollections is false, even when withExplicitNulls is true" ) { case class Test(list: List[Int], option: Option[Int]) - val helper = new FieldEncoder( + val helper = FieldEncoder( 1, "test", DeriveJsonEncoder.gen[Test], From 76d9c8f1a0b41ac08168fa940f5ff151ca961bb1 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 29 Jan 2025 16:54:14 +0100 Subject: [PATCH 116/311] Remove redundant dependencies for runtime (#1261) --- build.sbt | 16 +++++++++------- docs/index.md | 8 ++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/build.sbt b/build.sbt index b55b3ea27..073661c86 100644 --- a/build.sbt +++ b/build.sbt @@ -107,7 +107,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) libraryDependencies ++= Seq( "dev.zio" %%% "zio" % zioVersion, "dev.zio" %%% "zio-streams" % zioVersion, - "org.scala-lang.modules" %%% "scala-collection-compat" % "2.12.0", + "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.0" % "test", @@ -220,13 +220,14 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .jsSettings( libraryDependencies ++= Seq( "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, - "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion + "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % "test" ) ) .nativeSettings(nativeSettings) .nativeSettings( libraryDependencies ++= Seq( - "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion + "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, + "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % "test" ) ) .enablePlugins(BuildInfoPlugin) @@ -262,10 +263,11 @@ lazy val zioJsonYaml = project .settings(buildInfoSettings("zio.json.yaml")) .settings( libraryDependencies ++= Seq( - "org.yaml" % "snakeyaml" % "2.3", - "dev.zio" %% "zio" % zioVersion, - "dev.zio" %% "zio-test" % zioVersion % "test", - "dev.zio" %% "zio-test-sbt" % zioVersion % "test" + "org.yaml" % "snakeyaml" % "2.3", + "org.scala-lang.modules" %% "scala-collection-compat" % "2.13.0", + "dev.zio" %% "zio" % zioVersion, + "dev.zio" %% "zio-test" % zioVersion % "test", + "dev.zio" %% "zio-test-sbt" % zioVersion % "test" ), testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) diff --git a/docs/index.md b/docs/index.md index 9707491aa..e5819369d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,14 @@ In order to use this library, we need to add the following line in our `build.sb libraryDependencies += "dev.zio" %% "zio-json" % "@VERSION@" ``` +For cross-platform projects with Scala.js and Scala Native need to replace `%%` operator by `%%%`, +and optionally when using `java.time.ZoneId` and `java.time.ZonedDateTime` types need to add +the dependency on the latest version of Timezone DB: + +```scala +libraryDependencies += "io.github.cquiroz" %%% "scala-java-time-tzdb" % "latest.integration" +``` + ## Example Let's try a simple example of encoding and decoding JSON using ZIO JSON. From 18783d51aed92ab92de14e27f3ebf4664d9bee7f Mon Sep 17 00:00:00 2001 From: Paul Daniels Date: Thu, 30 Jan 2025 00:19:18 +0800 Subject: [PATCH 117/311] Add both variants (#1260) --- .../src/main/scala/zio/json/JsonDecoder.scala | 20 +++++++++++++++++++ .../src/test/scala/zio/json/DecoderSpec.scala | 13 ++++++++++++ 2 files changed, 33 insertions(+) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index eec450142..baa62fd1b 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -59,6 +59,26 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { */ final def <*[B](that: => JsonDecoder[B]): JsonDecoder[A] = self.zipLeft(that) + final def both[B](that: => JsonDecoder[B]): JsonDecoder[(A, B)] = + bothWith(that)((a, b) => (a, b)) + + final def bothRight[B](that: => JsonDecoder[B]): JsonDecoder[B] = + bothWith(that)((_, b) => b) + + final def bothLeft[B](that: => JsonDecoder[B]): JsonDecoder[A] = + bothWith(that)((a, _) => a) + + final def bothWith[B, C](that: => JsonDecoder[B])(f: (A, B) => C): JsonDecoder[C] = + new JsonDecoder[C] { + override def unsafeDecode(trace: List[JsonError], in: RetractReader): C = { + val in2 = new WithRecordingReader(in, 64) + val a = self.unsafeDecode(trace, in2) + in2.rewind() + val b = that.unsafeDecode(trace, in2) + f(a, b) + } + } + /** * Attempts to decode a value of type `A` from the specified `CharSequence`, but may fail with a human-readable error * message if the provided text does not encode a value of this type. diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 7afaeaa80..7263b5252 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -491,6 +491,19 @@ object DecoderSpec extends ZIOSpecDefault { ) ) ) + }, + test("bothWith") { + final case class Foo(a: Int) + final case class Bar(b: String) + + val fooDecoder: JsonDecoder[Foo] = DeriveJsonDecoder.gen + val barDecoder: JsonDecoder[Bar] = DeriveJsonDecoder.gen + implicit val fooAndBarDecoder: JsonDecoder[(Foo, Bar)] = fooDecoder.both(barDecoder) + + val json = """{"a": 1, "b": "foo"}""" + assertTrue( + json.fromJson[(Foo, Bar)] == Right((Foo(1), Bar("foo"))) + ) } ), suite("fromJsonAST")( From 7dd7fae4b9321b3fd6448681e9525982dff8a7dc Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 29 Jan 2025 18:09:26 +0100 Subject: [PATCH 118/311] Update Scala 3 to 3.3.5 and clean up MiMa settings (#1263) --- .github/workflows/ci.yml | 4 ++-- build.sbt | 14 ++++++++++++++ project/BuildHelper.scala | 20 +++----------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69d560b99..787b927c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: java: ['11', '21'] - scala: ['2.13.16', '3.3.4'] + scala: ['2.13.16', '3.3.5'] steps: - name: Checkout current branch uses: actions/checkout@v4.1.2 @@ -79,7 +79,7 @@ jobs: fail-fast: false matrix: java: ['11', '21'] - scala: ['2.12.20', '2.13.16', '3.3.4'] + scala: ['2.12.20', '2.13.16', '3.3.5'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch diff --git a/build.sbt b/build.sbt index 073661c86..2a07d3376 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,6 @@ import BuildHelper.* +import com.typesafe.tools.mima.core.Problem +import com.typesafe.tools.mima.core.ProblemFilters.exclude import com.typesafe.tools.mima.plugin.MimaKeys.mimaPreviousArtifacts import explicitdeps.ExplicitDepsPlugin.autoImport.moduleFilterRemoveValue import sbtcrossproject.CrossPlugin.autoImport.crossProject @@ -218,6 +220,12 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) ) .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) .jsSettings( + mimaBinaryIssueFilters ++= Seq( + exclude[Problem]("zio.JsonPackagePlatformSpecific.*"), + exclude[Problem]("zio.json.JsonDecoderPlatformSpecific.*"), + exclude[Problem]("zio.json.JsonEncoderPlatformSpecific.*"), + exclude[Problem]("zio.json.package.*") + ), libraryDependencies ++= Seq( "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % "test" @@ -225,6 +233,12 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) ) .nativeSettings(nativeSettings) .nativeSettings( + mimaBinaryIssueFilters ++= Seq( + exclude[Problem]("zio.JsonPackagePlatformSpecific.*"), + exclude[Problem]("zio.json.JsonDecoderPlatformSpecific.*"), + exclude[Problem]("zio.json.JsonEncoderPlatformSpecific.*"), + exclude[Problem]("zio.json.package.*") + ), libraryDependencies ++= Seq( "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % "test" diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 5de7f2eef..5b3bd69ca 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -28,7 +28,7 @@ object BuildHelper { } val Scala212: String = versions("2.12") val Scala213: String = versions("2.13") - val ScalaDotty: String = "3.3.4" + val ScalaDotty: String = "3.3.5" val SilencerVersion = "1.7.19" @@ -244,18 +244,10 @@ object BuildHelper { incOptions ~= (_.withLogRecompileOnMacro(false)), autoAPIMappings := true, unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library"), - mimaPreviousArtifacts := { - previousStableVersion.value.filter(_ != "0.7.4").map(organization.value %% name.value % _).toSet ++ - Set(organization.value %% name.value % "0.7.3") - }, - mimaCheckDirection := "backward", // TODO: find how we can use "both" for path versions + mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% name.value % _).toSet, + mimaCheckDirection := "backward", // TODO: find how we can use "both" for patch versions of 1.x releases mimaBinaryIssueFilters ++= Seq( - exclude[Problem]("zio.json.macros#package."), - exclude[Problem]("zio.JsonPackagePlatformSpecific.*"), - exclude[Problem]("zio.json.JsonDecoderPlatformSpecific.*"), - exclude[Problem]("zio.json.JsonEncoderPlatformSpecific.*"), exclude[Problem]("zio.json.internal.*"), - exclude[Problem]("zio.json.package.*"), exclude[Problem]("zio.json.yaml.internal.*") ), mimaFailOnProblem := true @@ -291,12 +283,6 @@ object BuildHelper { val scalaJavaTimeVersion = "2.6.0" - def jsSettings = - Seq( - libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, - libraryDependencies += "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion - ) - def nativeSettings = Seq( nativeConfig ~= { cfg => import scala.scalanative.build.{ GC, Mode } From b5356f022ceb9e248c6f7987deaccdb87fc974c0 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 30 Jan 2025 12:23:48 +0100 Subject: [PATCH 119/311] More efficient parsing of floats and doubles (#1265) --- .../scala/zio/json/internal/SafeNumbers.scala | 16 +- .../scala/zio/json/internal/SafeNumbers.scala | 16 +- .../scala/zio/json/internal/SafeNumbers.scala | 16 +- .../main/scala/zio/json/internal/lexer.scala | 40 +-- .../scala/zio/json/internal/numbers.scala | 268 +++++++++--------- .../zio/json/internal/SafeNumbersSpec.scala | 27 +- 6 files changed, 188 insertions(+), 195 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 84d72280b..36e34fc19 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -42,41 +42,41 @@ object SafeNumbers { def byte(num: String): ByteOption = try ByteSome(UnsafeNumbers.byte(num)) - catch { case UnsafeNumber => ByteNone } + catch { case _: UnexpectedEnd | UnsafeNumber => ByteNone } def short(num: String): ShortOption = try ShortSome(UnsafeNumbers.short(num)) - catch { case UnsafeNumber => ShortNone } + catch { case _: UnexpectedEnd | UnsafeNumber => ShortNone } def int(num: String): IntOption = try IntSome(UnsafeNumbers.int(num)) - catch { case UnsafeNumber => IntNone } + catch { case _: UnexpectedEnd | UnsafeNumber => IntNone } def long(num: String): LongOption = try LongSome(UnsafeNumbers.long(num)) - catch { case UnsafeNumber => LongNone } + catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } def bigInteger( num: String, max_bits: Int = 128 ): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) - catch { case UnsafeNumber => None } + catch { case _: UnexpectedEnd | UnsafeNumber => None } def float(num: String, max_bits: Int = 128): FloatOption = try FloatSome(UnsafeNumbers.float(num, max_bits)) - catch { case UnsafeNumber => FloatNone } + catch { case _: UnexpectedEnd | UnsafeNumber => FloatNone } def double(num: String, max_bits: Int = 128): DoubleOption = try DoubleSome(UnsafeNumbers.double(num, max_bits)) - catch { case UnsafeNumber => DoubleNone } + catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } def bigDecimal( num: String, max_bits: Int = 128 ): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) - catch { case UnsafeNumber => None } + catch { case _: UnexpectedEnd | UnsafeNumber => None } // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index 0bb7ba4af..c2404faf2 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -42,41 +42,41 @@ object SafeNumbers { def byte(num: String): ByteOption = try ByteSome(UnsafeNumbers.byte(num)) - catch { case UnsafeNumber => ByteNone } + catch { case _: UnexpectedEnd | UnsafeNumber => ByteNone } def short(num: String): ShortOption = try ShortSome(UnsafeNumbers.short(num)) - catch { case UnsafeNumber => ShortNone } + catch { case _: UnexpectedEnd | UnsafeNumber => ShortNone } def int(num: String): IntOption = try IntSome(UnsafeNumbers.int(num)) - catch { case UnsafeNumber => IntNone } + catch { case _: UnexpectedEnd | UnsafeNumber => IntNone } def long(num: String): LongOption = try LongSome(UnsafeNumbers.long(num)) - catch { case UnsafeNumber => LongNone } + catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } def bigInteger( num: String, max_bits: Int = 128 ): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) - catch { case UnsafeNumber => None } + catch { case _: UnexpectedEnd | UnsafeNumber => None } def float(num: String, max_bits: Int = 128): FloatOption = try FloatSome(UnsafeNumbers.float(num, max_bits)) - catch { case UnsafeNumber => FloatNone } + catch { case _: UnexpectedEnd | UnsafeNumber => FloatNone } def double(num: String, max_bits: Int = 128): DoubleOption = try DoubleSome(UnsafeNumbers.double(num, max_bits)) - catch { case UnsafeNumber => DoubleNone } + catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } def bigDecimal( num: String, max_bits: Int = 128 ): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) - catch { case UnsafeNumber => None } + catch { case _: UnexpectedEnd | UnsafeNumber => None } // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index 26f3c480a..8691066ed 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -42,41 +42,41 @@ object SafeNumbers { def byte(num: String): ByteOption = try ByteSome(UnsafeNumbers.byte(num)) - catch { case UnsafeNumber => ByteNone } + catch { case _: UnexpectedEnd | UnsafeNumber => ByteNone } def short(num: String): ShortOption = try ShortSome(UnsafeNumbers.short(num)) - catch { case UnsafeNumber => ShortNone } + catch { case _: UnexpectedEnd | UnsafeNumber => ShortNone } def int(num: String): IntOption = try IntSome(UnsafeNumbers.int(num)) - catch { case UnsafeNumber => IntNone } + catch { case _: UnexpectedEnd | UnsafeNumber => IntNone } def long(num: String): LongOption = try LongSome(UnsafeNumbers.long(num)) - catch { case UnsafeNumber => LongNone } + catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } def bigInteger( num: String, max_bits: Int = 128 ): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) - catch { case UnsafeNumber => None } + catch { case _: UnexpectedEnd | UnsafeNumber => None } def float(num: String, max_bits: Int = 128): FloatOption = try FloatSome(UnsafeNumbers.float(num, max_bits)) - catch { case UnsafeNumber => FloatNone } + catch { case _: UnexpectedEnd | UnsafeNumber => FloatNone } def double(num: String, max_bits: Int = 128): DoubleOption = try DoubleSome(UnsafeNumbers.double(num, max_bits)) - catch { case UnsafeNumber => DoubleNone } + catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } def bigDecimal( num: String, max_bits: Int = 128 ): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) - catch { case UnsafeNumber => None } + catch { case _: UnexpectedEnd | UnsafeNumber => None } // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index ba7b7b5c9..bbdbef9f9 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -285,9 +285,7 @@ object Lexer { } } - def byte(trace: List[JsonError], in: RetractReader): Byte = { - in.nextNonWhitespace() - in.retract() + def byte(trace: List[JsonError], in: RetractReader): Byte = try { val i = UnsafeNumbers.byte_(in, false) in.retract() @@ -295,11 +293,8 @@ object Lexer { } catch { case UnsafeNumbers.UnsafeNumber => error("expected a Byte", trace) } - } - def short(trace: List[JsonError], in: RetractReader): Short = { - in.nextNonWhitespace() - in.retract() + def short(trace: List[JsonError], in: RetractReader): Short = try { val i = UnsafeNumbers.short_(in, false) in.retract() @@ -307,11 +302,8 @@ object Lexer { } catch { case UnsafeNumbers.UnsafeNumber => error("expected a Short", trace) } - } - def int(trace: List[JsonError], in: RetractReader): Int = { - in.nextNonWhitespace() - in.retract() + def int(trace: List[JsonError], in: RetractReader): Int = try { val i = UnsafeNumbers.int_(in, false) in.retract() @@ -319,11 +311,8 @@ object Lexer { } catch { case UnsafeNumbers.UnsafeNumber => error("expected an Int", trace) } - } - def long(trace: List[JsonError], in: RetractReader): Long = { - in.nextNonWhitespace() - in.retract() + def long(trace: List[JsonError], in: RetractReader): Long = try { val i = UnsafeNumbers.long_(in, false) in.retract() @@ -331,14 +320,11 @@ object Lexer { } catch { case UnsafeNumbers.UnsafeNumber => error("expected a Long", trace) } - } def bigInteger( trace: List[JsonError], in: RetractReader - ): java.math.BigInteger = { - in.nextNonWhitespace() - in.retract() + ): java.math.BigInteger = try { val i = UnsafeNumbers.bigInteger_(in, false, NumberMaxBits) in.retract() @@ -346,11 +332,8 @@ object Lexer { } catch { case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits bit BigInteger", trace) } - } - def float(trace: List[JsonError], in: RetractReader): Float = { - in.nextNonWhitespace() - in.retract() + def float(trace: List[JsonError], in: RetractReader): Float = try { val i = UnsafeNumbers.float_(in, false, NumberMaxBits) in.retract() @@ -358,11 +341,8 @@ object Lexer { } catch { case UnsafeNumbers.UnsafeNumber => error("expected a Float", trace) } - } - def double(trace: List[JsonError], in: RetractReader): Double = { - in.nextNonWhitespace() - in.retract() + def double(trace: List[JsonError], in: RetractReader): Double = try { val i = UnsafeNumbers.double_(in, false, NumberMaxBits) in.retract() @@ -370,14 +350,11 @@ object Lexer { } catch { case UnsafeNumbers.UnsafeNumber => error("expected a Double", trace) } - } def bigDecimal( trace: List[JsonError], in: RetractReader - ): java.math.BigDecimal = { - in.nextNonWhitespace() - in.retract() + ): java.math.BigDecimal = try { val i = UnsafeNumbers.bigDecimal_(in, false, NumberMaxBits) in.retract() @@ -385,7 +362,6 @@ object Lexer { } catch { case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits BigDecimal", trace) } - } // optional whitespace and then an expected character @inline def char(trace: List[JsonError], in: OneCharReader, c: Char): Unit = { diff --git a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala index 7b84a5b34..eb9b5840f 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala @@ -15,7 +15,6 @@ */ package zio.json.internal -import java.io._ import scala.util.control.NoStackTrace // specialised Options to avoid boxing. Prefer .isEmpty guarded access to .value @@ -25,10 +24,12 @@ sealed abstract class ByteOption { def isEmpty: Boolean def value: Byte } + case object ByteNone extends ByteOption { def isEmpty = true def value: Byte = throw new java.util.NoSuchElementException } + case class ByteSome(value: Byte) extends ByteOption { def isEmpty = false } @@ -37,10 +38,12 @@ sealed abstract class ShortOption { def isEmpty: Boolean def value: Short } + case object ShortNone extends ShortOption { def isEmpty = true def value: Short = throw new java.util.NoSuchElementException } + case class ShortSome(value: Short) extends ShortOption { def isEmpty = false } @@ -49,10 +52,12 @@ sealed abstract class IntOption { def isEmpty: Boolean def value: Int } + case object IntNone extends IntOption { def isEmpty = true def value: Int = throw new java.util.NoSuchElementException } + case class IntSome(value: Int) extends IntOption { def isEmpty = false } @@ -61,10 +66,12 @@ sealed abstract class LongOption { def isEmpty: Boolean def value: Long } + case object LongNone extends LongOption { def isEmpty = true def value: Long = throw new java.util.NoSuchElementException } + case class LongSome(value: Long) extends LongOption { def isEmpty = false } @@ -73,10 +80,12 @@ sealed abstract class FloatOption { def isEmpty: Boolean def value: Float } + case object FloatNone extends FloatOption { def isEmpty = true def value: Float = throw new java.util.NoSuchElementException } + case class FloatSome(value: Float) extends FloatOption { def isEmpty = false } @@ -85,10 +94,12 @@ sealed abstract class DoubleOption { def isEmpty: Boolean def value: Double } + case object DoubleNone extends DoubleOption { def isEmpty = true def value: Double = throw new java.util.NoSuchElementException } + case class DoubleSome(value: Double) extends DoubleOption { def isEmpty = false } @@ -113,42 +124,50 @@ object UnsafeNumbers { def byte(num: String): Byte = byte_(new FastStringReader(num), true) - def byte_(in: Reader, consume: Boolean): Byte = - int__(in, -128, 127, consume).toByte + + def byte_(in: OneCharReader, consume: Boolean): Byte = { + val n = int_(in, consume) + if (n < -128 || n > 127) throw UnsafeNumber + n.toByte + } def short(num: String): Short = short_(new FastStringReader(num), true) - def short_(in: Reader, consume: Boolean): Short = - int__(in, -32768, 32767, consume).toShort + + def short_(in: OneCharReader, consume: Boolean): Short = { + val n = int_(in, consume) + if (n < -32768 || n > 32767) throw UnsafeNumber + n.toShort + } def int(num: String): Int = int_(new FastStringReader(num), true) - def int_(in: Reader, consume: Boolean): Int = - int__(in, -2147483648, 2147483647, consume).toInt def long(num: String): Long = long_(new FastStringReader(num), true) - def long_(in: Reader, consume: Boolean): Long = - long__(in, Long.MinValue, Long.MaxValue, consume) def bigInteger(num: String, max_bits: Int): java.math.BigInteger = bigInteger_(new FastStringReader(num), true, max_bits) + def bigInteger_( - in: Reader, + in: OneCharReader, consume: Boolean, max_bits: Int ): java.math.BigInteger = { - var current: Int = in.read() - val negative = current == '-' - if (negative) current = in.read() - if (current == -1) throw UnsafeNumber + var current = + if (consume) in.readChar() + else in.nextNonWhitespace() + val negative = current == '-' + if (negative) current = in.readChar() bigDecimal__(in, consume, negative, current, true, max_bits).unscaledValue } - def int__(in: Reader, lower: Int, upper: Int, consume: Boolean): Int = { - var current = in.read() + def int_(in: OneCharReader, consume: Boolean): Int = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt val negative = current == '-' - if (negative) current = in.read() + if (negative) current = in.readChar().toInt if (current < '0' || current > '9') throw UnsafeNumber var accum = '0' - current while ({ @@ -163,19 +182,17 @@ object UnsafeNumbers { ) throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (negative) { - if (accum < lower) throw UnsafeNumber - } else if (accum != -2147483648) { - accum = -accum - if (upper < accum) throw UnsafeNumber - } else throw UnsafeNumber - accum + if (negative) accum + else if (accum != -2147483648) -accum + else throw UnsafeNumber } - def long__(in: Reader, lower: Long, upper: Long, consume: Boolean): Long = { - var current = in.read() + def long_(in: OneCharReader, consume: Boolean): Long = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt val negative = current == '-' - if (negative) current = in.read() + if (negative) current = in.readChar().toInt if (current < '0' || current > '9') throw UnsafeNumber var accum = ('0' - current).toLong while ({ @@ -190,44 +207,34 @@ object UnsafeNumbers { ) throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (negative) { - if (accum < lower) throw UnsafeNumber - } else if (accum != -9223372036854775808L) { - accum = -accum - if (upper < accum) throw UnsafeNumber - } else throw UnsafeNumber - accum + if (negative) accum + else if (accum != -9223372036854775808L) -accum + else throw UnsafeNumber } def float(num: String, max_bits: Int): Float = float_(new FastStringReader(num), true, max_bits) - def float_(in: Reader, consume: Boolean, max_bits: Int): Float = { - var current = in.read() - var negative = false - + def float_(in: OneCharReader, consume: Boolean, max_bits: Int): Float = { + var current = + if (consume) in.readChar() + else in.nextNonWhitespace() if (current == 'N') { readAll(in, "aN", consume) return Float.NaN } - - negative = current == '-' - if (negative) current = in.read() - + val negative = current == '-' + if (negative) current = in.readChar() if (current == 'I' || current == '+') { if (current == '+') { - current = in.read() + current = in.readChar() if (current != 'I') throw UnsafeNumber } readAll(in, "nfinity", consume) if (negative) return Float.NegativeInfinity else return Float.PositiveInfinity } - - if (current == -1) throw UnsafeNumber - val res = bigDecimal__(in, consume, negative = negative, initial = current, int_only = false, max_bits = max_bits) - if (negative && res.unscaledValue == java.math.BigInteger.ZERO) -0.0f else res.floatValue } @@ -235,30 +242,25 @@ object UnsafeNumbers { def double(num: String, max_bits: Int): Double = double_(new FastStringReader(num), true, max_bits) - def double_(in: Reader, consume: Boolean, max_bits: Int): Double = { - var current = in.read() - var negative = false - + def double_(in: OneCharReader, consume: Boolean, max_bits: Int): Double = { + var current = + if (consume) in.readChar() + else in.nextNonWhitespace() if (current == 'N') { readAll(in, "aN", consume) return Double.NaN } - - negative = current == '-' - if (negative) current = in.read() - + val negative = current == '-' + if (negative) current = in.readChar() if (current == 'I' || current == '+') { if (current == '+') { - current = in.read() + current = in.readChar() if (current != 'I') throw UnsafeNumber } readAll(in, "nfinity", consume) if (negative) return Double.NegativeInfinity else return Double.PositiveInfinity } - - if (current == -1) throw UnsafeNumber - // we could avoid going via BigDecimal if we wanted to do something like // https://github.com/plokhotnyuk/jsoniter-scala/blob/56ff2a60e28aa27bd4788caf3b1557a558c00fa1/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala#L1395-L1425 // based on @@ -275,41 +277,42 @@ object UnsafeNumbers { else res.doubleValue } - private[this] def readAll(in: Reader, s: String, consume: Boolean): Unit = { - val len = s.length - var i, current = 0 + private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { + val len = s.length + var i = 0 while (i < len) { - current = in.read() - if (current != s(i)) throw UnsafeNumber + if (in.readChar() != s.charAt(i)) throw UnsafeNumber i += 1 } - current = in.read() // to be consistent read the terminator + val current = in.read() // to be consistent read the terminator if (consume && current != -1) throw UnsafeNumber } def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = bigDecimal_(new FastStringReader(num), true, max_bits) + def bigDecimal_( - in: Reader, + in: OneCharReader, consume: Boolean, max_bits: Int ): java.math.BigDecimal = { - var current: Int = in.read() - val negative = current == '-' - if (negative) current = in.read() - if (current == -1) throw UnsafeNumber + var current = + if (consume) in.readChar() + else in.nextNonWhitespace() + val negative = current == '-' + if (negative) current = in.readChar() bigDecimal__(in, consume, negative, current, false, max_bits) } def bigDecimal__( - in: Reader, + in: OneCharReader, consume: Boolean, negative: Boolean, - initial: Int, + initial: Char, int_only: Boolean, max_bits: Int ): java.math.BigDecimal = { - var current: Int = initial + var current: Int = initial.toInt // record the significand as Long until it overflows, then swap to BigInteger var sig: Long = -1 // -1 means it hasn't been seen yet var sig_ : java.math.BigInteger = null // non-null wins over sig @@ -324,58 +327,42 @@ object UnsafeNumbers { return java.math.BigDecimal.ZERO } - def push_sig(): Unit = { - val c = current - '0' - // would be nice if there was a fused instruction... + while ('0' <= current && current <= '9') { + val digit = current - '0' if (sig_ != null) { - sig_ = sig_ - .multiply(java.math.BigInteger.TEN) - .add(bigIntegers(c)) - // arbitrary limit on BigInteger size to avoid OOM attacks - if (sig_.bitLength >= max_bits) - throw UnsafeNumber + sig_ = sig_.multiply(java.math.BigInteger.TEN).add(bigIntegers(digit)) + if (sig_.bitLength >= max_bits) throw UnsafeNumber } else if (sig >= 922337203685477580L) - sig_ = java.math.BigInteger - .valueOf(sig) - .multiply(java.math.BigInteger.TEN) - .add(bigIntegers(c)) - else if (sig < 0) sig = c.toLong - else sig = sig * 10 + c - } - - def significand() = - if (sig <= 0) java.math.BigDecimal.ZERO - else { - val res = - if (sig_ != null) - new java.math.BigDecimal(sig_) - else - new java.math.BigDecimal(sig) - if (negative) res.negate else res - } - - while ('0' <= current && current <= '9') { - push_sig() + sig_ = java.math.BigInteger.valueOf(sig).multiply(java.math.BigInteger.TEN).add(bigIntegers(digit)) + else if (sig < 0) sig = digit.toLong + else sig = (sig << 3) + (sig << 1) + digit current = in.read() if (current == -1) - return significand() + return significand(sig, sig_, negative, 0) } if (int_only) { - if (consume && current != -1) - throw UnsafeNumber - return significand() + if (consume && current != -1) throw UnsafeNumber + return significand(sig, sig_, negative, 0) } if (current == '.') { if (sig < 0) sig = 0 // e.g. ".1" is shorthand for "0.1" current = in.read() if (current == -1) - return significand() + return significand(sig, sig_, negative, 0) while ('0' <= current && current <= '9') { dot += 1 - if (sig > 0 || current != '0') - push_sig() + if (sig > 0 || current != '0') { + val digit = current - '0' + if (sig_ != null) { + sig_ = sig_.multiply(java.math.BigInteger.TEN).add(bigIntegers(digit)) + if (sig_.bitLength >= max_bits) throw UnsafeNumber + } else if (sig >= 922337203685477580L) + sig_ = java.math.BigInteger.valueOf(sig).multiply(java.math.BigInteger.TEN).add(bigIntegers(digit)) + else if (sig < 0) sig = digit.toLong + else sig = (sig << 3) + (sig << 1) + digit + } // overflowed... if (dot < 0) throw UnsafeNumber current = in.read() @@ -386,37 +373,58 @@ object UnsafeNumbers { if (current == 'E' || current == 'e') { current = in.read() - val negative = current == '-' - if (negative || current == '+') current = in.read() + val negativeExp = current == '-' + if (negativeExp || current == '+') current = in.read() if (current < '0' || current > '9') throw UnsafeNumber - var accum = '0' - current + exp = '0' - current while ({ current = in.read() '0' <= current && current <= '9' }) { if ( - accum < -214748364 || { - accum = accum * 10 + ('0' - current) - accum > 0 + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 } ) throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (negative) { - exp = accum - } else if (accum != -2147483648) { - exp = -accum - } else throw UnsafeNumber - } else if (consume && current != -1) - throw UnsafeNumber - - val scale = if (dot < 1) exp else exp - dot - val res = significand() - if (scale != 0) - res.scaleByPowerOfTen(scale) - else - res + if (negativeExp) {} + else if (exp != -2147483648) exp = -exp + else throw UnsafeNumber + } else if (consume && current != -1) throw UnsafeNumber + + significand( + sig, + sig_, + negative, + if (dot < 1) -exp + else dot - exp + ) } + + @inline private[this] def significand( + sig: Long, + sig_ : java.math.BigInteger, + negative: Boolean, + scale: Int + ): java.math.BigDecimal = + if (sig <= 0) java.math.BigDecimal.ZERO + else if (sig_ != null) { + new java.math.BigDecimal( + { + if (negative) sig_.negate + else sig_ + }, + scale + ) + } else + java.math.BigDecimal.valueOf( + if (negative) -sig + else sig, + scale + ) + // note that bigDecimal does not have a negative zero private[this] val bigIntegers: Array[java.math.BigInteger] = (0L to 9L).map(java.math.BigInteger.valueOf).toArray diff --git a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala index c5c91525d..b0a4f93e5 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala @@ -41,11 +41,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { ) check(Gen.fromIterable(invalidBigDecimalEdgeCases)) { s => - assert(SafeNumbers.bigDecimal(s).map(_.toString))( - isSome( - equalTo((new java.math.BigDecimal(s)).toString) - ) - ) + assert(SafeNumbers.bigDecimal(s).get.compareTo(new java.math.BigDecimal(s)))(equalTo(0)) } }, test("invalid BigDecimal text") { @@ -65,7 +61,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(Gen.fromIterable(inputs)) { s => assert(SafeNumbers.bigInteger(s))( isSome( - equalTo((new java.math.BigInteger(s))) + equalTo(new java.math.BigInteger(s)) ) ) } @@ -202,6 +198,19 @@ object SafeNumbersSpec extends ZIOSpecDefault { test("valid") { check(Gen.int)(d => assert(SafeNumbers.int(d.toString))(equalTo(IntSome(d)))) }, + test("invalid (edge cases)") { + val input = List( + "1e3", + "1E-2", + "0.1", + "", + "1 ", + "-2147483649", + "2147483648" + ) + + check(Gen.fromIterable(input))(x => assert(SafeNumbers.int(x))(equalTo(IntNone))) + }, test("invalid (out of range)") { check(Gen.long.filter(i => i < Int.MinValue || i > Int.MaxValue))(d => assert(SafeNumbers.int(d.toString))(equalTo(IntNone)) @@ -217,10 +226,10 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(Gen.fromIterable(input))(x => assert(SafeNumbers.long(x))(equalTo(LongSome(x.toLong)))) }, - test("in valid edge cases") { + test("invalid (edge cases)") { val input = List( - "0foo", - "01foo", + "1e3foo", + "1E-2", "0.1", "", "1 ", From fb203f647bf959801de544a081e99aa63951af6c Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 30 Jan 2025 16:26:07 +0100 Subject: [PATCH 120/311] More efficient decoding of chars (#1266) --- .../src/main/scala/zio/json/JsonDecoder.scala | 6 +---- .../main/scala/zio/json/internal/lexer.scala | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index baa62fd1b..43272c230 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -309,11 +309,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with } implicit val char: JsonDecoder[Char] = new JsonDecoder[Char] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Char = { - val s = Lexer.string(trace, in) - if (s.length == 1) s.charAt(0) - else Lexer.error("expected single character string", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Char = Lexer.char(trace, in) override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Char = json match { diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index bbdbef9f9..03491d9c5 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -250,6 +250,30 @@ object Lexer { sb.buffer } + def char(trace: List[JsonError], in: OneCharReader): Char = { + var c = in.nextNonWhitespace() + if (c != '"') error("'\"'", c, trace) + c = in.readChar() + if (c == '"') error("expected single character string", trace) + else if (c == '\\') { + (in.readChar(): @switch) match { + case '"' => c = '"' + case '\\' => c = '\\' + case '/' => c = '/' + case 'b' => c = '\b' + case 'f' => c = '\f' + case 'n' => c = '\n' + case 'r' => c = '\r' + case 't' => c = '\t' + case 'u' => c = nextHex4(trace, in) + case _ => error(c, trace) + } + } else if (c < ' ') error("invalid control in string", trace) + val c1 = in.readChar() + if (c1 != '"') error("expected single character string", trace) + c + } + // consumes 4 hex characters after current @noinline def nextHex4(trace: List[JsonError], in: OneCharReader): Char = { From c014aaac1c686ac9f3a3442e463101a9c48676e5 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 31 Jan 2025 06:10:19 +0100 Subject: [PATCH 121/311] More efficient decoding of floats and doubles (#1267) --- .../scala/zio/json/internal/numbers.scala | 412 ++++++++++-------- 1 file changed, 237 insertions(+), 175 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala index eb9b5840f..1a7c5f1c3 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala @@ -143,31 +143,12 @@ object UnsafeNumbers { def int(num: String): Int = int_(new FastStringReader(num), true) - def long(num: String): Long = - long_(new FastStringReader(num), true) - - def bigInteger(num: String, max_bits: Int): java.math.BigInteger = - bigInteger_(new FastStringReader(num), true, max_bits) - - def bigInteger_( - in: OneCharReader, - consume: Boolean, - max_bits: Int - ): java.math.BigInteger = { - var current = - if (consume) in.readChar() - else in.nextNonWhitespace() - val negative = current == '-' - if (negative) current = in.readChar() - bigDecimal__(in, consume, negative, current, true, max_bits).unscaledValue - } - def int_(in: OneCharReader, consume: Boolean): Int = { var current = if (consume) in.readChar().toInt else in.nextNonWhitespace().toInt - val negative = current == '-' - if (negative) current = in.readChar().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt if (current < '0' || current > '9') throw UnsafeNumber var accum = '0' - current while ({ @@ -182,17 +163,20 @@ object UnsafeNumbers { ) throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (negative) accum + if (negate) accum else if (accum != -2147483648) -accum else throw UnsafeNumber } + def long(num: String): Long = + long_(new FastStringReader(num), true) + def long_(in: OneCharReader, consume: Boolean): Long = { var current = if (consume) in.readChar().toInt else in.nextNonWhitespace().toInt - val negative = current == '-' - if (negative) current = in.readChar().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt if (current < '0' || current > '9') throw UnsafeNumber var accum = ('0' - current).toLong while ({ @@ -207,36 +191,128 @@ object UnsafeNumbers { ) throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (negative) accum + if (negate) accum else if (accum != -9223372036854775808L) -accum else throw UnsafeNumber } + def bigInteger(num: String, max_bits: Int): java.math.BigInteger = + bigInteger_(new FastStringReader(num), true, max_bits) + + def bigInteger_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigInteger = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var bigSig: java.math.BigInteger = null + var sig = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (sig < 922337203685477580L) sig = (sig << 3) + (sig << 1) + (current - '0') + else { + if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) + bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigSig.bitLength >= max_bits) throw UnsafeNumber + } + } + if (consume && current != -1) throw UnsafeNumber + if (bigSig eq null) { + if (negate) sig = -sig + return java.math.BigInteger.valueOf(sig) + } + if (negate) bigSig = bigSig.negate + bigSig + } + def float(num: String, max_bits: Int): Float = float_(new FastStringReader(num), true, max_bits) def float_(in: OneCharReader, consume: Boolean, max_bits: Int): Float = { var current = - if (consume) in.readChar() - else in.nextNonWhitespace() + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt if (current == 'N') { readAll(in, "aN", consume) return Float.NaN } - val negative = current == '-' - if (negative) current = in.readChar() + val negate = current == '-' + if (negate) current = in.readChar().toInt if (current == 'I' || current == '+') { if (current == '+') { - current = in.readChar() + current = in.readChar().toInt if (current != 'I') throw UnsafeNumber } readAll(in, "nfinity", consume) - if (negative) return Float.NegativeInfinity - else return Float.PositiveInfinity + return if (negate) Float.NegativeInfinity else Float.PositiveInfinity + } + var sig = -1L + var bigSig: java.math.BigInteger = null + if ('0' <= current && current <= '9') { + sig = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (sig < 922337203685477580L) sig = (sig << 3) + (sig << 1) + (current - '0') + else { + if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) + bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigSig.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var scale, exp = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + scale += 1 + if (sig < 922337203685477580L) { + if (sig < 0) sig = (current - '0').toLong + else sig = (sig << 3) + (sig << 1) + (current - '0') + } else { + if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) + bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigSig.bitLength >= max_bits) throw UnsafeNumber + } + } } - val res = bigDecimal__(in, consume, negative = negative, initial = current, int_only = false, max_bits = max_bits) - if (negative && res.unscaledValue == java.math.BigInteger.ZERO) -0.0f - else res.floatValue + if (sig < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) {} + else if (exp != -2147483648) exp = -exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (sig == 0) { + return if (negate) -0.0f else 0.0f + } else if (bigSig eq null) { + if (negate) sig = -sig + return java.math.BigDecimal.valueOf(sig, scale - exp).floatValue() + } + if (negate) bigSig = bigSig.negate + new java.math.BigDecimal(bigSig, scale - exp).floatValue() } def double(num: String, max_bits: Int): Double = @@ -244,137 +320,135 @@ object UnsafeNumbers { def double_(in: OneCharReader, consume: Boolean, max_bits: Int): Double = { var current = - if (consume) in.readChar() - else in.nextNonWhitespace() + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt if (current == 'N') { readAll(in, "aN", consume) return Double.NaN } - val negative = current == '-' - if (negative) current = in.readChar() + val negate = current == '-' + if (negate) current = in.readChar().toInt if (current == 'I' || current == '+') { if (current == '+') { - current = in.readChar() + current = in.readChar().toInt if (current != 'I') throw UnsafeNumber } readAll(in, "nfinity", consume) - if (negative) return Double.NegativeInfinity - else return Double.PositiveInfinity + return if (negate) Double.NegativeInfinity else Double.PositiveInfinity } - // we could avoid going via BigDecimal if we wanted to do something like - // https://github.com/plokhotnyuk/jsoniter-scala/blob/56ff2a60e28aa27bd4788caf3b1557a558c00fa1/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala#L1395-L1425 - // based on - // https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct - // - // the fallback of .doubleValue tends to call out to parseDouble which - // ultimately uses strtod from the system libraries and they may loop until - // the answer converges - // https://github.com/rust-lang/rust/pull/27307/files#diff-fe6c36003393c49bf7e5c413458d6d9cR43-R84 - val res = bigDecimal__(in, consume, negative, current, false, max_bits) - // BigDecimal doesn't have a negative zero, so we need to apply manually - if (negative && res.unscaledValue == java.math.BigInteger.ZERO) -0.0 - // TODO implement Algorithm M or Bigcomp and avoid going via BigDecimal - else res.doubleValue - } - - private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { - val len = s.length - var i = 0 - while (i < len) { - if (in.readChar() != s.charAt(i)) throw UnsafeNumber - i += 1 + var sig = -1L + var bigSig: java.math.BigInteger = null + if ('0' <= current && current <= '9') { + sig = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (sig < 922337203685477580L) sig = (sig << 3) + (sig << 1) + (current - '0') + else { + if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) + bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigSig.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var scale, exp = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + scale += 1 + if (sig < 922337203685477580L) { + if (sig < 0) sig = (current - '0').toLong + else sig = (sig << 3) + (sig << 1) + (current - '0') + } else { + if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) + bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigSig.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (sig < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) {} + else if (exp != -2147483648) exp = -exp + else throw UnsafeNumber } - val current = in.read() // to be consistent read the terminator if (consume && current != -1) throw UnsafeNumber + if (sig == 0) { + return if (negate) -0.0 else 0.0 + } else if (bigSig eq null) { + if (negate) sig = -sig + return java.math.BigDecimal.valueOf(sig, scale - exp).doubleValue() + } + if (negate) bigSig = bigSig.negate + new java.math.BigDecimal(bigSig, scale - exp).doubleValue() } def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = bigDecimal_(new FastStringReader(num), true, max_bits) - def bigDecimal_( - in: OneCharReader, - consume: Boolean, - max_bits: Int - ): java.math.BigDecimal = { + def bigDecimal_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigDecimal = { var current = - if (consume) in.readChar() - else in.nextNonWhitespace() - val negative = current == '-' - if (negative) current = in.readChar() - bigDecimal__(in, consume, negative, current, false, max_bits) - } - - def bigDecimal__( - in: OneCharReader, - consume: Boolean, - negative: Boolean, - initial: Char, - int_only: Boolean, - max_bits: Int - ): java.math.BigDecimal = { - var current: Int = initial.toInt - // record the significand as Long until it overflows, then swap to BigInteger - var sig: Long = -1 // -1 means it hasn't been seen yet - var sig_ : java.math.BigInteger = null // non-null wins over sig - var dot: Int = 0 // counts from the right - var exp: Int = 0 // implied - - // skip trailing zero on the left - while (current == '0') { - sig = 0 - current = in.read() - if (current == -1) - return java.math.BigDecimal.ZERO - } - - while ('0' <= current && current <= '9') { - val digit = current - '0' - if (sig_ != null) { - sig_ = sig_.multiply(java.math.BigInteger.TEN).add(bigIntegers(digit)) - if (sig_.bitLength >= max_bits) throw UnsafeNumber - } else if (sig >= 922337203685477580L) - sig_ = java.math.BigInteger.valueOf(sig).multiply(java.math.BigInteger.TEN).add(bigIntegers(digit)) - else if (sig < 0) sig = digit.toLong - else sig = (sig << 3) + (sig << 1) + digit - current = in.read() - if (current == -1) - return significand(sig, sig_, negative, 0) - } - - if (int_only) { - if (consume && current != -1) throw UnsafeNumber - return significand(sig, sig_, negative, 0) + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + var bigSig: java.math.BigInteger = null + var sig = -1L + if ('0' <= current && current <= '9') { + sig = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (sig < 922337203685477580L) sig = (sig << 3) + (sig << 1) + (current - '0') + else { + if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) + bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigSig.bitLength >= max_bits) throw UnsafeNumber + } + } } - + var scale, exp = 0 if (current == '.') { - if (sig < 0) sig = 0 // e.g. ".1" is shorthand for "0.1" - current = in.read() - if (current == -1) - return significand(sig, sig_, negative, 0) - while ('0' <= current && current <= '9') { - dot += 1 - if (sig > 0 || current != '0') { - val digit = current - '0' - if (sig_ != null) { - sig_ = sig_.multiply(java.math.BigInteger.TEN).add(bigIntegers(digit)) - if (sig_.bitLength >= max_bits) throw UnsafeNumber - } else if (sig >= 922337203685477580L) - sig_ = java.math.BigInteger.valueOf(sig).multiply(java.math.BigInteger.TEN).add(bigIntegers(digit)) - else if (sig < 0) sig = digit.toLong - else sig = (sig << 3) + (sig << 1) + digit - } - // overflowed... - if (dot < 0) throw UnsafeNumber + while ({ current = in.read() + '0' <= current && current <= '9' + }) { + scale += 1 + if (sig < 922337203685477580L) { + if (sig < 0) sig = (current - '0').toLong + else sig = (sig << 3) + (sig << 1) + (current - '0') + } else { + if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) + bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigSig.bitLength >= max_bits) throw UnsafeNumber + } } } - - if (sig < 0) throw UnsafeNumber // no significand - - if (current == 'E' || current == 'e') { - current = in.read() - val negativeExp = current == '-' - if (negativeExp || current == '+') current = in.read() + if (sig < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt if (current < '0' || current > '9') throw UnsafeNumber exp = '0' - current while ({ @@ -388,42 +462,30 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (consume && current != -1) throw UnsafeNumber - if (negativeExp) {} + if (negateExp) {} else if (exp != -2147483648) exp = -exp else throw UnsafeNumber - } else if (consume && current != -1) throw UnsafeNumber - - significand( - sig, - sig_, - negative, - if (dot < 1) -exp - else dot - exp - ) + } + if (consume && current != -1) throw UnsafeNumber + if (bigSig eq null) { + if (negate) sig = -sig + return java.math.BigDecimal.valueOf(sig, scale - exp) + } + if (negate) bigSig = bigSig.negate + new java.math.BigDecimal(bigSig, scale - exp) } - @inline private[this] def significand( - sig: Long, - sig_ : java.math.BigInteger, - negative: Boolean, - scale: Int - ): java.math.BigDecimal = - if (sig <= 0) java.math.BigDecimal.ZERO - else if (sig_ != null) { - new java.math.BigDecimal( - { - if (negative) sig_.negate - else sig_ - }, - scale - ) - } else - java.math.BigDecimal.valueOf( - if (negative) -sig - else sig, - scale - ) + @noinline + private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { + val len = s.length + var i = 0 + while (i < len) { + if (in.readChar() != s.charAt(i)) throw UnsafeNumber + i += 1 + } + val current = in.read() // to be consistent read the terminator + if (consume && current != -1) throw UnsafeNumber + } // note that bigDecimal does not have a negative zero private[this] val bigIntegers: Array[java.math.BigInteger] = From 9de04f4abe2a4ce0b2079c76d425249c74ec77d4 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 31 Jan 2025 11:56:06 +0100 Subject: [PATCH 122/311] Add fast and medium paths for decoding of floats and doubles (#1268) --- .../zio/json/internal/UnsafeNumbers.scala | 658 ++++++++++++++++++ .../zio/json/internal/UnsafeNumbers.scala | 655 +++++++++++++++++ .../zio/json/internal/UnsafeNumbers.scala | 649 +++++++++++++++++ .../scala/zio/json/internal/numbers.scala | 390 ----------- .../zio/json/internal/SafeNumbersSpec.scala | 247 ++++--- 5 files changed, 2130 insertions(+), 469 deletions(-) create mode 100644 zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala create mode 100644 zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala create mode 100644 zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala new file mode 100644 index 000000000..24e25633a --- /dev/null +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -0,0 +1,658 @@ +/* + * Copyright 2019-2022 John A. De Goes and the ZIO Contributors + * + * 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 zio.json.internal + +import scala.util.control.NoStackTrace + +// The underlying implementation uses an exception that has no stack trace for +// the failure case, which is 20x faster than retaining stack traces. Therefore, +// we require no boxing of the results on the happy path. This slows down the +// unhappy path a little bit, but it's still on the same order of magnitude as +// the happy path. +// +// This API should only be used by people who know what they are doing. Note +// that Reader implementations consume one character beyond the number that is +// parsed, because there is no terminator character. +object UnsafeNumbers { + + // should never escape into user code + case object UnsafeNumber + extends Exception("if you see this a dev made a mistake using UnsafeNumbers") + with NoStackTrace + + def byte(num: String): Byte = + byte_(new FastStringReader(num), true) + + def byte_(in: OneCharReader, consume: Boolean): Byte = { + val n = int_(in, consume) + if (n < -128 || n > 127) throw UnsafeNumber + n.toByte + } + + def short(num: String): Short = + short_(new FastStringReader(num), true) + + def short_(in: OneCharReader, consume: Boolean): Short = { + val n = int_(in, consume) + if (n < -32768 || n > 32767) throw UnsafeNumber + n.toShort + } + + def int(num: String): Int = + int_(new FastStringReader(num), true) + + def int_(in: OneCharReader, consume: Boolean): Int = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var accum = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -214748364 || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (negate) accum + else if (accum != -2147483648) -accum + else throw UnsafeNumber + } + + def long(num: String): Long = + long_(new FastStringReader(num), true) + + def long_(in: OneCharReader, consume: Boolean): Long = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var accum = ('0' - current).toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -922337203685477580L || { + accum = (accum << 3) + (accum << 1) + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (negate) accum + else if (accum != -9223372036854775808L) -accum + else throw UnsafeNumber + } + + def bigInteger(num: String, max_bits: Int): java.math.BigInteger = + bigInteger_(new FastStringReader(num), true, max_bits) + + def bigInteger_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigInteger = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var bigM10: java.math.BigInteger = null + var m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + if (negate) m10 = -m10 + return java.math.BigInteger.valueOf(m10) + } + if (negate) bigM10 = bigM10.negate + bigM10 + } + + def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = + bigDecimal_(new FastStringReader(num), true, max_bits) + + def bigDecimal_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigDecimal = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + var bigM10: java.math.BigInteger = null + var m10 = -1L + if ('0' <= current && current <= '9') { + m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var e10 = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + e10 -= 1 + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (m10 < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) e10 += exp + else if (exp != -2147483648) e10 -= exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + if (negate) m10 = -m10 + return java.math.BigDecimal.valueOf(m10, -e10) + } + if (negate) bigM10 = bigM10.negate + new java.math.BigDecimal(bigM10, -e10) + } + + def float(num: String, max_bits: Int): Float = + float_(new FastStringReader(num), true, max_bits) + + def float_(in: OneCharReader, consume: Boolean, max_bits: Int): Float = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + if (current == 'N') { + readAll(in, "aN", consume) + return Float.NaN + } + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current == 'I' || current == '+') { + if (current == '+') { + current = in.readChar().toInt + if (current != 'I') throw UnsafeNumber + } + readAll(in, "nfinity", consume) + return if (negate) Float.NegativeInfinity else Float.PositiveInfinity + } + var digits = 1 // calculate digits for m10 only + var m10 = -1L + var bigM10: java.math.BigInteger = null + if ('0' <= current && current <= '9') { + m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var e10 = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + e10 -= 1 + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (m10 < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) e10 += exp + else if (exp != -2147483648) e10 -= exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + var x: Float = + if (e10 == 0) m10.toFloat + else { + if (m10 < 4294967296L && e10 >= digits - 23 && e10 <= 19 - digits) { + val pow10 = pow10Doubles + (if (e10 < 0) m10 / pow10(-e10) + else m10 * pow10(e10)).toFloat + } else toFloat(m10, e10) + } + if (negate) x = -x + return x + } + if (negate) bigM10 = bigM10.negate + new java.math.BigDecimal(bigM10, -e10).floatValue() + } + + // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical + // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct + @noinline private[this] def toFloat(m10: Long, e10: Int): Float = + if (m10 == 0 || e10 < -64) 0.0f + else if (e10 >= 39) Float.PositiveInfinity + else { + var shift = java.lang.Long.numberOfLeadingZeros(m10) + var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 + shift = java.lang.Long.numberOfLeadingZeros(m2) + m2 <<= shift + e2 -= shift + val truncatedBitNum = Math.max(-149 - e2, 40) + val savedBitNum = 64 - truncatedBitNum + val mask = -1L >>> Math.max(savedBitNum, 0) + val halfwayDiff = (m2 & mask) - (mask >>> 1) + if (Math.abs(halfwayDiff) > 1 || savedBitNum <= 0) java.lang.Float.intBitsToFloat { + var mf = 0 + if (savedBitNum > 0) mf = (m2 >>> truncatedBitNum).toInt + e2 += truncatedBitNum + if (savedBitNum >= 0 && halfwayDiff > 0) { + if (mf == 0xffffff) { + mf = 0x800000 + e2 += 1 + } else mf += 1 + } + if (e2 == -149) mf + else if (e2 >= 105) 0x7f800000 + else e2 + 150 << 23 | mf & 0x7fffff + } + else java.math.BigDecimal.valueOf(m10, -e10).floatValue() + } + + def double(num: String, max_bits: Int): Double = + double_(new FastStringReader(num), true, max_bits) + + def double_(in: OneCharReader, consume: Boolean, max_bits: Int): Double = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + if (current == 'N') { + readAll(in, "aN", consume) + return Double.NaN + } + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current == 'I' || current == '+') { + if (current == '+') { + current = in.readChar().toInt + if (current != 'I') throw UnsafeNumber + } + readAll(in, "nfinity", consume) + return if (negate) Double.NegativeInfinity else Double.PositiveInfinity + } + var digits = 1 // calculate digits for m10 only + var m10 = -1L + var bigM10: java.math.BigInteger = null + if ('0' <= current && current <= '9') { + m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var e10 = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + e10 -= 1 + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (m10 < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) e10 += exp + else if (exp != -2147483648) e10 -= exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + var x: Double = + if (e10 == 0) m10.toDouble + else { + if (m10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - digits) { + val pow10 = pow10Doubles + if (e10 < 0) m10 / pow10(-e10) + else if (e10 <= 22) m10 * pow10(e10) + else { + val slop = 16 - digits + (m10 * pow10(slop)) * pow10(e10 - slop) + } + } else toDouble(m10, e10) + } + if (negate) x = -x + return x + } + if (negate) bigM10 = bigM10.negate + new java.math.BigDecimal(bigM10, -e10).doubleValue() + } + + // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical + // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct + @inline private[this] def toDouble(m10: Long, e10: Int): Double = + if (m10 == 0 || e10 < -343) 0.0 + else if (e10 >= 310) Double.PositiveInfinity + else { + var shift = java.lang.Long.numberOfLeadingZeros(m10) + var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 + shift = java.lang.Long.numberOfLeadingZeros(m2) + m2 <<= shift + e2 -= shift + val truncatedBitNum = Math.max(-1074 - e2, 11) + val savedBitNum = 64 - truncatedBitNum + val mask = -1L >>> Math.max(savedBitNum, 0) + val halfwayDiff = (m2 & mask) - (mask >>> 1) + if (Math.abs(halfwayDiff) > 1 || savedBitNum <= 0) java.lang.Double.longBitsToDouble { + if (savedBitNum <= 0) m2 = 0 + m2 >>>= truncatedBitNum + e2 += truncatedBitNum + if (savedBitNum >= 0 && halfwayDiff > 0) { + if (m2 == 0x1fffffffffffffL) { + m2 = 0x10000000000000L + e2 += 1 + } else m2 += 1 + } + if (e2 == -1074) m2 + else if (e2 >= 972) 0x7ff0000000000000L + else (e2 + 1075).toLong << 52 | m2 & 0xfffffffffffffL + } + else java.math.BigDecimal.valueOf(m10, -e10).doubleValue() + } + + @noinline private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { + val len = s.length + var i = 0 + while (i < len) { + if (in.readChar() != s.charAt(i)) throw UnsafeNumber + i += 1 + } + val current = in.read() // to be consistent read the terminator + if (consume && current != -1) throw UnsafeNumber + } + + // 64-bit unsigned multiplication was adopted from the great Hacker's Delight function + // (Henry S. Warren, Hacker's Delight, Addison-Wesley, 2nd edition, Fig. 8.2) + // https://doc.lagout.org/security/Hackers%20Delight.pdf + @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = { + val xl = x & 0xffffffffL + val xh = x >>> 32 + val yl = y & 0xffffffffL + val yh = y >>> 32 + val t = xh * yl + (xl * yl >>> 32) + xh * yh + (t >>> 32) + (xl * yh + (t & 0xffffffffL) >>> 32) + } + + private[this] final val bigIntegers: Array[java.math.BigInteger] = + (0L to 9L).map(java.math.BigInteger.valueOf).toArray + + private[this] final val pow10Doubles: Array[Double] = + Array(1, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, + 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22) + + private[this] final val pow10Mantissas: Array[Long] = Array( + -4671960508600951122L, -1228264617323800998L, -7685194413468457480L, -4994806998408183946L, -1631822729582842029L, + -7937418233630358124L, -5310086773610559751L, -2025922448585811785L, -8183730558007214222L, -5617977179081629873L, + -2410785455424649437L, -8424269937281487754L, -5918651403174471789L, -2786628235540701832L, -8659171674854020501L, + -6212278575140137722L, -3153662200497784248L, -8888567902952197011L, -6499023860262858360L, -3512093806901185046L, + -9112587656954322510L, -6779048552765515233L, -3862124672529506138L, -215969822234494768L, -7052510166537641086L, + -4203951689744663454L, -643253593753441413L, -7319562523736982739L, -4537767136243840520L, -1060522901877412746L, + -7580355841314464822L, -4863758783215693124L, -1468012460592228501L, -7835036815511224669L, -5182110000961642932L, + -1865951482774665761L, -8083748704375247957L, -5492999862041672042L, -2254563809124702148L, -8326631408344020699L, + -5796603242002637969L, -2634068034075909558L, -8563821548938525330L, -6093090917745768758L, -3004677628754823043L, + -8795452545612846258L, -6382629663588669919L, -3366601061058449494L, -9021654690802612790L, -6665382345075878084L, + -3720041912917459700L, -38366372719436721L, -6941508010590729807L, -4065198994811024355L, -469812725086392539L, + -7211161980820077193L, -4402266457597708587L, -891147053569747830L, -7474495936122174250L, -4731433901725329908L, + -1302606358729274481L, -7731658001846878407L, -5052886483881210105L, -1704422086424124727L, -7982792831656159810L, + -5366805021142811859L, -2096820258001126919L, -8228041688891786181L, -5673366092687344822L, -2480021597431793123L, + -8467542526035952558L, -5972742139117552794L, -2854241655469553088L, -8701430062309552536L, -6265101559459552766L, + -3219690930897053053L, -8929835859451740015L, -6550608805887287114L, -3576574988931720989L, -9152888395723407474L, + -6829424476226871438L, -3925094576856201394L, -294682202642863838L, -7101705404292871755L, -4265445736938701790L, + -720121152745989333L, -7367604748107325189L, -4597819916706768583L, -1135588877456072824L, -7627272076051127371L, + -4922404076636521310L, -1541319077368263733L, -7880853450996246689L, -5239380795317920458L, -1937539975720012668L, + -8128491512466089774L, -5548928372155224313L, -2324474446766642487L, -8370325556870233411L, -5851220927660403859L, + -2702340141148116920L, -8606491615858654931L, -6146428501395930760L, -3071349608317525546L, -8837122532839535322L, + -6434717147622031249L, -3431710416100151157L, -9062348037703676329L, -6716249028702207507L, -3783625267450371480L, + -117845565885576446L, -6991182506319567135L, -4127292114472071014L, -547429124662700864L, -7259672230555269896L, + -4462904269766699466L, -966944318780986428L, -7521869226879198374L, -4790650515171610063L, -1376627125537124675L, + -7777920981101784778L, -5110715207949843068L, -1776707991509915931L, -8027971522334779313L, -5423278384491086237L, + -2167411962186469893L, -8272161504007625539L, -5728515861582144020L, -2548958808550292121L, -8510628282985014432L, + -6026599335303880135L, -2921563150702462265L, -8743505996830120772L, -6317696477610263061L, -3285434578585440922L, + -8970925639256982432L, -6601971030643840136L, -3640777769877412266L, -9193015133814464522L, -6879582898840692749L, + -3987792605123478032L, -373054737976959636L, -7150688238876681629L, -4326674280168464132L, -796656831783192261L, + -7415439547505577019L, -4657613415954583370L, -1210330751515841308L, -7673985747338482674L, -4980796165745715438L, + -1614309188754756393L, -7926472270612804602L, -5296404319838617848L, -2008819381370884406L, -8173041140997884610L, + -5604615407819967859L, -2394083241347571919L, -8413831053483314306L, -5905602798426754978L, -2770317479606055818L, + -8648977452394866743L, -6199535797066195524L, -3137733727905356501L, -8878612607581929669L, -6486579741050024183L, + -3496538657885142324L, -9102865688819295809L, -6766896092596731857L, -3846934097318526917L, -196981603220770742L, + -7040642529654063570L, -4189117143640191558L, -624710411122851544L, -7307973034592864071L, -4523280274813692185L, + -1042414325089727327L, -7569037980822161435L, -4849611457600313890L, -1450328303573004458L, -7823984217374209643L, + -5168294253290374149L, -1848681798185579782L, -8072955151507069220L, -5479507920956448621L, -2237698882768172872L, + -8316090829371189901L, -5783427518286599473L, -2617598379430861437L, -8553528014785370254L, -6080224000054324913L, + -2988593981640518238L, -8785400266166405755L, -6370064314280619289L, -3350894374423386208L, -9011838011655698236L, + -6653111496142234891L, -3704703351750405709L, -19193171260619233L, -6929524759678968877L, -4050219931171323192L, + -451088895536766085L, -7199459587351560659L, -4387638465762062920L, -872862063775190746L, -7463067817500576073L, + -4717148753448332187L, -1284749923383027329L, -7720497729755473937L, -5038936143766954517L, -1686984161281305242L, + -7971894128441897632L, -5353181642124984136L, -2079791034228842266L, -8217398424034108273L, -5660062011615247437L, + -2463391496091671392L, -8457148712698376476L, -5959749872445582691L, -2838001322129590460L, -8691279853972075893L, + -6252413799037706963L, -3203831230369745799L, -8919923546622172981L, -6538218414850328322L, -3561087000135522498L, + -9143208402725783417L, -6817324484979841368L, -3909969587797413806L, -275775966319379353L, -7089889006590693952L, + -4250675239810979535L, -701658031336336515L, -7356065297226292178L, -4583395603105477319L, -1117558485454458744L, + -7616003081050118571L, -4908317832885260310L, -1523711272679187483L, -7869848573065574033L, -5225624697904579637L, + -1920344853953336643L, -8117744561361917258L, -5535494683275008668L, -2307682335666372931L, -8359830487432564938L, + -5838102090863318269L, -2685941595151759932L, -8596242524610931813L, -6133617137336276863L, -3055335403242958174L, + -8827113654667930715L, -6422206049907525490L, -3416071543957018958L, -9052573742614218705L, -6704031159840385477L, + -3768352931373093942L, -98755145788979524L, -6979250993759194058L, -4112377723771604669L, -528786136287117932L, + -7248020362820530564L, -4448339435098275301L, -948738275445456222L, -7510490449794491995L, -4776427043815727089L, + -1358847786342270957L, -7766808894105001205L, -5096825099203863602L, -1759345355577441598L, -8017119874876982855L, + -5409713825168840664L, -2150456263033662926L, -8261564192037121185L, -5715269221619013577L, -2532400508596379068L, + -8500279345513818773L, -6013663163464885563L, -2905392935903719049L, -8733399612580906262L, -6305063497298744923L, + -3269643353196043250L, -8961056123388608887L, -6589634135808373205L, -3625356651333078602L, -9183376934724255983L, + -6867535149977932074L, -3972732919045027189L, -354230130378896082L, -7138922859127891907L, -4311967555482476980L, + -778273425925708321L, -7403949918844649557L, -4643251380128424042L, -1192378206733142148L, -7662765406849295699L, + -4966770740134231719L, -1596777406740401745L, -7915514906853832947L, -5282707615139903279L, -1991698500497491195L, + -8162340590452013853L, -5591239719637629412L, -2377363631119648861L, -8403381297090862394L, -5892540602936190089L, + -2753989735242849707L, -8638772612167862923L, -6186779746782440750L, -3121788665050663033L, -8868646943297746252L, + -6474122660694794911L, -3480967307441105734L, -9093133594791772940L, -6754730975062328271L, -3831727700400522434L, + -177973607073265139L, -7028762532061872568L, -4174267146649952806L, -606147914885053103L, -7296371474444240046L, + -4508778324627912153L, -1024286887357502287L, -7557708332239520786L, -4835449396872013078L, -1432625727662628443L, + -7812920107430224633L, -5154464115860392887L, -1831394126398103205L, -8062150356639896359L, -5466001927372482545L, + -2220816390788215277L, -8305539271883716405L, -5770238071427257602L, -2601111570856684098L, -8543223759426509417L, + -6067343680855748868L, -2972493582642298180L, -8775337516792518219L, -6357485877563259869L, -3335171328526686933L, + -9002011107970261189L, -6640827866535438582L, -3689348814741910324L, -9223372036854775808L, -6917529027641081856L, + -4035225266123964416L, -432345564227567616L, -7187745005283311616L, -4372995238176751616L, -854558029293551616L, + -7451627795949551616L, -4702848726509551616L, -1266874889709551616L, -7709325833709551616L, -5024971273709551616L, + -1669528073709551616L, -7960984073709551616L, -5339544073709551616L, -2062744073709551616L, -8206744073709551616L, + -5646744073709551616L, -2446744073709551616L, -8446744073709551616L, -5946744073709551616L, -2821744073709551616L, + -8681119073709551616L, -6239712823709551616L, -3187955011209551616L, -8910000909647051616L, -6525815118631426616L, + -3545582879861895366L, -9133518327554766460L, -6805211891016070171L, -3894828845342699810L, -256850038250986858L, + -7078060301547948643L, -4235889358507547899L, -683175679707046970L, -7344513827457986212L, -4568956265895094861L, + -1099509313941480672L, -7604722348854507276L, -4894216917640746191L, -1506085128623544835L, -7858832233030797378L, + -5211854272861108819L, -1903131822648998119L, -8106986416796705681L, -5522047002568494197L, -2290872734783229842L, + -8349324486880600507L, -5824969590173362730L, -2669525969289315508L, -8585982758446904049L, -6120792429631242157L, + -3039304518611664792L, -8817094351773372351L, -6409681921289327535L, -3400416383184271515L, -9042789267131251553L, + -6691800565486676537L, -3753064688430957767L, -79644842111309304L, -6967307053960650171L, -4097447799023424810L, + -510123730351893109L, -7236356359111015049L, -4433759430461380907L, -930513269649338230L, -7499099821171918250L, + -4762188758037509908L, -1341049929119499481L, -7755685233340769032L, -5082920523248573386L, -1741964635633328828L, + -8006256924911912374L, -5396135137712502563L, -2133482903713240300L, -8250955842461857044L, -5702008784649933400L, + -2515824962385028846L, -8489919629131724885L, -6000713517987268202L, -2889205879056697349L, -8723282702051517699L, + -6292417359137009220L, -3253835680493873621L, -8951176327949752869L, -6577284391509803182L, -3609919470959866074L, + -9173728696990998152L, -6855474852811359786L, -3957657547586811828L, -335385916056126881L, -7127145225176161157L, + -4297245513042813542L, -759870872876129024L, -7392448323188662496L, -4628874385558440216L, -1174406963520662366L, + -7651533379841495835L, -4952730706374481889L, -1579227364540714458L, -7904546130479028392L, -5268996644671397586L, + -1974559787411859078L, -8151628894773493780L, -5577850100039479321L, -2360626606621961247L, -8392920656779807636L, + -5879464802547371641L, -2737644984756826647L, -8628557143114098510L, -6174010410465235234L, -3105826994654156138L, + -8858670899299929442L, -6461652605697523899L, -3465379738694516970L, -9083391364325154962L, -6742553186979055799L, + -3816505465296431844L, -158945813193151901L, -7016870160886801794L, -4159401682681114339L, -587566084924005019L, + -7284757830718584993L, -4494261269970843337L, -1006140569036166268L, -7546366883288685774L, -4821272585683469313L, + -1414904713676948737L, -7801844473689174817L, -5140619573684080617L, -1814088448677712867L, -8051334308064652398L, + -5452481866653427593L, -2203916314889396588L, -8294976724446954723L, -5757034887131305500L, -2584607590486743971L, + -8532908771695296838L, -6054449946191733143L, -2956376414312278525L, -8765264286586255934L, -6344894339805432014L, + -3319431906329402113L, -8992173969096958177L, -6628531442943809817L, -3673978285252374367L, -9213765455923815836L, + -6905520801477381891L, -4020214983419339459L, -413582710846786420L, -7176018221920323369L, -4358336758973016307L, + -836234930288882479L, -7440175859071633406L, -4688533805412153853L, -1248981238337804412L, -7698142301602209614L, + -5010991858575374113L, -1652053804791829737L, -7950062655635975442L, -5325892301117581398L, -2045679357969588844L, + -8196078626372074883L, -5633412264537705700L, -2430079312244744221L, -8436328597794046994L, -5933724728815170839L, + -2805469892591575644L, -8670947710510816634L, -6226998619711132888L, -3172062256211528206L, -8900067937773286985L, + -6513398903789220827L, -3530062611309138130L, -9123818159709293187L, -6793086681209228580L, -3879672333084147821L, + -237904397927796872L, -7066219276345954901L, -4221088077005055722L, -664674077828931749L, -7332950326284164199L, + -4554501889427817345L, -1081441343357383777L, -7593429867239446717L, -4880101315621920492L, -1488440626100012711L, + -7847804418953589800L, -5198069505264599346L, -1885900863153361279L, -8096217067111932656L, -5508585315462527915L, + -2274045625900771990L, -8338807543829064350L, -5811823411358942533L, -2653093245771290262L, -8575712306248138270L, + -6107954364382784934L, -3023256937051093263L, -8807064613298015146L, -6397144748195131028L, -3384744916816525881L, + -9032994600651410532L, -6679557232386875260L, -3737760522056206171L, -60514634142869810L, -6955350673980375487L, + -4082502324048081455L, -491441886632713915L, -7224680206786528053L, -4419164240055772162L, -912269281642327298L, + -7487697328667536418L, -4747935642407032618L, -1323233534581402868L, -7744549986754458649L, -5069001465015685407L, + -1724565812842218855L, -7995382660667468640L, -5382542307406947896L, -2116491865831296966L, -8240336443785642460L, + -5688734536304665171L, -2499232151953443560L, -8479549122611984081L, -5987750384837592197L, -2873001962619602342L, + -8713155254278333320L, -6279758049420528746L, -3238011543348273028L, -8941286242233752499L, -6564921784364802720L, + -3594466212028615495L, -9164070410158966541L, -6843401994271320272L, -3942566474411762436L, -316522074587315140L, + -7115355324258153819L, -4282508136895304370L, -741449152691742558L, -7380934748073420955L, -4614482416664388289L, + -1156417002403097458L, -7640289654143017767L, -4938676049251384305L, -1561659043136842477L, -7893565929601608404L, + -5255271393574622601L, -1957403223540890347L, -8140906042354138323L, -5564446534515285000L, -2343872149716718346L, + -8382449121214030822L, -5866375383090150624L, -2721283210435300376L, -8618331034163144591L, -6161227774276542835L, + -3089848699418290639L, -8848684464777513506L, -6449169562544503978L, -3449775934753242068L, -9073638986861858149L, + -6730362715149934782L, -3801267375510030573L, -139898200960150313L, -7004965403241175802L, -4144520735624081848L, + -568964901102714406L, -7273132090830278360L, -4479729095110460046L, -987975350460687153L, -7535013621679011327L, + -4807081008671376254L, -1397165242411832414L, -7790757304148477115L, -5126760611758208489L, -1796764746270372707L, + -8040506994060064798L, -5438947724147693094L, -2186998636757228463L, -8284403175614349646L, -5743817951090549153L, + -2568086420435798537L, -8522583040413455942L, -6041542782089432023L, -2940242459184402125L, -8755180564631333184L, + -6332289687361778576L, -3303676090774835316L, -8982326584375353929L, -6616222212041804507L, -3658591746624867729L, + -9204148869281624187L, -6893500068174642330L, -4005189066790915008L, -394800315061255856L, -7164279224554366766L, + -4343663012265570553L, -817892746904575288L, -7428711994456441411L, -4674203974643163860L, -1231068949876566920L, + -7686947121313936181L, -4996997883215032323L, -1634561335591402499L, -7939129862385708418L, -5312226309554747619L, + -2028596868516046619L, -8185402070463610993L, -5620066569652125837L + ) +} diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala new file mode 100644 index 000000000..e0745b186 --- /dev/null +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -0,0 +1,655 @@ +/* + * Copyright 2019-2022 John A. De Goes and the ZIO Contributors + * + * 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 zio.json.internal + +import scala.util.control.NoStackTrace + +// The underlying implementation uses an exception that has no stack trace for +// the failure case, which is 20x faster than retaining stack traces. Therefore, +// we require no boxing of the results on the happy path. This slows down the +// unhappy path a little bit, but it's still on the same order of magnitude as +// the happy path. +// +// This API should only be used by people who know what they are doing. Note +// that Reader implementations consume one character beyond the number that is +// parsed, because there is no terminator character. +object UnsafeNumbers { + + // should never escape into user code + case object UnsafeNumber + extends Exception("if you see this a dev made a mistake using UnsafeNumbers") + with NoStackTrace + + def byte(num: String): Byte = + byte_(new FastStringReader(num), true) + + def byte_(in: OneCharReader, consume: Boolean): Byte = { + val n = int_(in, consume) + if (n < -128 || n > 127) throw UnsafeNumber + n.toByte + } + + def short(num: String): Short = + short_(new FastStringReader(num), true) + + def short_(in: OneCharReader, consume: Boolean): Short = { + val n = int_(in, consume) + if (n < -32768 || n > 32767) throw UnsafeNumber + n.toShort + } + + def int(num: String): Int = + int_(new FastStringReader(num), true) + + def int_(in: OneCharReader, consume: Boolean): Int = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var accum = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -214748364 || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (negate) accum + else if (accum != -2147483648) -accum + else throw UnsafeNumber + } + + def long(num: String): Long = + long_(new FastStringReader(num), true) + + def long_(in: OneCharReader, consume: Boolean): Long = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var accum = ('0' - current).toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -922337203685477580L || { + accum = (accum << 3) + (accum << 1) + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (negate) accum + else if (accum != -9223372036854775808L) -accum + else throw UnsafeNumber + } + + def bigInteger(num: String, max_bits: Int): java.math.BigInteger = + bigInteger_(new FastStringReader(num), true, max_bits) + + def bigInteger_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigInteger = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var bigM10: java.math.BigInteger = null + var m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + if (negate) m10 = -m10 + return java.math.BigInteger.valueOf(m10) + } + if (negate) bigM10 = bigM10.negate + bigM10 + } + + def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = + bigDecimal_(new FastStringReader(num), true, max_bits) + + def bigDecimal_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigDecimal = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + var bigM10: java.math.BigInteger = null + var m10 = -1L + if ('0' <= current && current <= '9') { + m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var e10 = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + e10 -= 1 + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (m10 < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) e10 += exp + else if (exp != -2147483648) e10 -= exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + if (negate) m10 = -m10 + return java.math.BigDecimal.valueOf(m10, -e10) + } + if (negate) bigM10 = bigM10.negate + new java.math.BigDecimal(bigM10, -e10) + } + + def float(num: String, max_bits: Int): Float = + float_(new FastStringReader(num), true, max_bits) + + def float_(in: OneCharReader, consume: Boolean, max_bits: Int): Float = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + if (current == 'N') { + readAll(in, "aN", consume) + return Float.NaN + } + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current == 'I' || current == '+') { + if (current == '+') { + current = in.readChar().toInt + if (current != 'I') throw UnsafeNumber + } + readAll(in, "nfinity", consume) + return if (negate) Float.NegativeInfinity else Float.PositiveInfinity + } + var digits = 1 // calculate digits for m10 only + var m10 = -1L + var bigM10: java.math.BigInteger = null + if ('0' <= current && current <= '9') { + m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var e10 = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + e10 -= 1 + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (m10 < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) e10 += exp + else if (exp != -2147483648) e10 -= exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + var x: Float = + if (e10 == 0) m10.toFloat + else { + if (m10 < 4294967296L && e10 >= digits - 23 && e10 <= 19 - digits) { + val pow10 = pow10Doubles + (if (e10 < 0) m10 / pow10(-e10) + else m10 * pow10(e10)).toFloat + } else toFloat(m10, e10) + } + if (negate) x = -x + return x + } + if (negate) bigM10 = bigM10.negate + new java.math.BigDecimal(bigM10, -e10).floatValue() + } + + // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical + // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct + private[this] def toFloat(m10: Long, e10: Int): Float = + if (m10 == 0 || e10 < -64) 0.0f + else if (e10 >= 39) Float.PositiveInfinity + else { + var shift = java.lang.Long.numberOfLeadingZeros(m10) + var m2 = unsignedMultiplyHigh( + pow10Mantissas(e10 + 343), + m10 << shift + ) // FIXME: Use Math.unsignedMultiplyHigh after dropping of JDK 17 support + var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 + shift = java.lang.Long.numberOfLeadingZeros(m2) + m2 <<= shift + e2 -= shift + val truncatedBitNum = Math.max(-149 - e2, 40) + val savedBitNum = 64 - truncatedBitNum + val mask = -1L >>> Math.max(savedBitNum, 0) + val halfwayDiff = (m2 & mask) - (mask >>> 1) + if (Math.abs(halfwayDiff) > 1 || savedBitNum <= 0) java.lang.Float.intBitsToFloat { + var mf = 0 + if (savedBitNum > 0) mf = (m2 >>> truncatedBitNum).toInt + e2 += truncatedBitNum + if (savedBitNum >= 0 && halfwayDiff > 0) { + if (mf == 0xffffff) { + mf = 0x800000 + e2 += 1 + } else mf += 1 + } + if (e2 == -149) mf + else if (e2 >= 105) 0x7f800000 + else e2 + 150 << 23 | mf & 0x7fffff + } + else java.math.BigDecimal.valueOf(m10, -e10).floatValue() + } + + def double(num: String, max_bits: Int): Double = + double_(new FastStringReader(num), true, max_bits) + + def double_(in: OneCharReader, consume: Boolean, max_bits: Int): Double = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + if (current == 'N') { + readAll(in, "aN", consume) + return Double.NaN + } + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current == 'I' || current == '+') { + if (current == '+') { + current = in.readChar().toInt + if (current != 'I') throw UnsafeNumber + } + readAll(in, "nfinity", consume) + return if (negate) Double.NegativeInfinity else Double.PositiveInfinity + } + var digits = 1 // calculate digits for m10 only + var m10 = -1L + var bigM10: java.math.BigInteger = null + if ('0' <= current && current <= '9') { + m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var e10 = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + e10 -= 1 + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (m10 < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) e10 += exp + else if (exp != -2147483648) e10 -= exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + var x: Double = + if (e10 == 0) m10.toDouble + else { + if (m10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - digits) { + val pow10 = pow10Doubles + if (e10 < 0) m10 / pow10(-e10) + else if (e10 <= 22) m10 * pow10(e10) + else { + val slop = 16 - digits + (m10 * pow10(slop)) * pow10(e10 - slop) + } + } else toDouble(m10, e10) + } + if (negate) x = -x + return x + } + if (negate) bigM10 = bigM10.negate + new java.math.BigDecimal(bigM10, -e10).doubleValue() + } + + // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical + // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct + private[this] def toDouble(m10: Long, e10: Int): Double = + if (m10 == 0 || e10 < -343) 0.0 + else if (e10 >= 310) Double.PositiveInfinity + else { + var shift = java.lang.Long.numberOfLeadingZeros(m10) + var m2 = unsignedMultiplyHigh( + pow10Mantissas(e10 + 343), + m10 << shift + ) // FIXME: Use Math.unsignedMultiplyHigh after dropping of JDK 17 support + var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 + shift = java.lang.Long.numberOfLeadingZeros(m2) + m2 <<= shift + e2 -= shift + val truncatedBitNum = Math.max(-1074 - e2, 11) + val savedBitNum = 64 - truncatedBitNum + val mask = -1L >>> Math.max(savedBitNum, 0) + val halfwayDiff = (m2 & mask) - (mask >>> 1) + if (Math.abs(halfwayDiff) > 1 || savedBitNum <= 0) java.lang.Double.longBitsToDouble { + if (savedBitNum <= 0) m2 = 0 + m2 >>>= truncatedBitNum + e2 += truncatedBitNum + if (savedBitNum >= 0 && halfwayDiff > 0) { + if (m2 == 0x1fffffffffffffL) { + m2 = 0x10000000000000L + e2 += 1 + } else m2 += 1 + } + if (e2 == -1074) m2 + else if (e2 >= 972) 0x7ff0000000000000L + else (e2 + 1075).toLong << 52 | m2 & 0xfffffffffffffL + } + else java.math.BigDecimal.valueOf(m10, -e10).doubleValue() + } + + @noinline private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { + val len = s.length + var i = 0 + while (i < len) { + if (in.readChar() != s.charAt(i)) throw UnsafeNumber + i += 1 + } + val current = in.read() // to be consistent read the terminator + if (consume && current != -1) throw UnsafeNumber + } + + @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = + Math.multiplyHigh(x, y) + x + y // Use implementation that works only when both params are negative + + private[this] final val bigIntegers: Array[java.math.BigInteger] = + (0L to 9L).map(java.math.BigInteger.valueOf).toArray + + private[this] final val pow10Doubles: Array[Double] = + Array(1, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, + 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22) + + private[this] final val pow10Mantissas: Array[Long] = Array( + -4671960508600951122L, -1228264617323800998L, -7685194413468457480L, -4994806998408183946L, -1631822729582842029L, + -7937418233630358124L, -5310086773610559751L, -2025922448585811785L, -8183730558007214222L, -5617977179081629873L, + -2410785455424649437L, -8424269937281487754L, -5918651403174471789L, -2786628235540701832L, -8659171674854020501L, + -6212278575140137722L, -3153662200497784248L, -8888567902952197011L, -6499023860262858360L, -3512093806901185046L, + -9112587656954322510L, -6779048552765515233L, -3862124672529506138L, -215969822234494768L, -7052510166537641086L, + -4203951689744663454L, -643253593753441413L, -7319562523736982739L, -4537767136243840520L, -1060522901877412746L, + -7580355841314464822L, -4863758783215693124L, -1468012460592228501L, -7835036815511224669L, -5182110000961642932L, + -1865951482774665761L, -8083748704375247957L, -5492999862041672042L, -2254563809124702148L, -8326631408344020699L, + -5796603242002637969L, -2634068034075909558L, -8563821548938525330L, -6093090917745768758L, -3004677628754823043L, + -8795452545612846258L, -6382629663588669919L, -3366601061058449494L, -9021654690802612790L, -6665382345075878084L, + -3720041912917459700L, -38366372719436721L, -6941508010590729807L, -4065198994811024355L, -469812725086392539L, + -7211161980820077193L, -4402266457597708587L, -891147053569747830L, -7474495936122174250L, -4731433901725329908L, + -1302606358729274481L, -7731658001846878407L, -5052886483881210105L, -1704422086424124727L, -7982792831656159810L, + -5366805021142811859L, -2096820258001126919L, -8228041688891786181L, -5673366092687344822L, -2480021597431793123L, + -8467542526035952558L, -5972742139117552794L, -2854241655469553088L, -8701430062309552536L, -6265101559459552766L, + -3219690930897053053L, -8929835859451740015L, -6550608805887287114L, -3576574988931720989L, -9152888395723407474L, + -6829424476226871438L, -3925094576856201394L, -294682202642863838L, -7101705404292871755L, -4265445736938701790L, + -720121152745989333L, -7367604748107325189L, -4597819916706768583L, -1135588877456072824L, -7627272076051127371L, + -4922404076636521310L, -1541319077368263733L, -7880853450996246689L, -5239380795317920458L, -1937539975720012668L, + -8128491512466089774L, -5548928372155224313L, -2324474446766642487L, -8370325556870233411L, -5851220927660403859L, + -2702340141148116920L, -8606491615858654931L, -6146428501395930760L, -3071349608317525546L, -8837122532839535322L, + -6434717147622031249L, -3431710416100151157L, -9062348037703676329L, -6716249028702207507L, -3783625267450371480L, + -117845565885576446L, -6991182506319567135L, -4127292114472071014L, -547429124662700864L, -7259672230555269896L, + -4462904269766699466L, -966944318780986428L, -7521869226879198374L, -4790650515171610063L, -1376627125537124675L, + -7777920981101784778L, -5110715207949843068L, -1776707991509915931L, -8027971522334779313L, -5423278384491086237L, + -2167411962186469893L, -8272161504007625539L, -5728515861582144020L, -2548958808550292121L, -8510628282985014432L, + -6026599335303880135L, -2921563150702462265L, -8743505996830120772L, -6317696477610263061L, -3285434578585440922L, + -8970925639256982432L, -6601971030643840136L, -3640777769877412266L, -9193015133814464522L, -6879582898840692749L, + -3987792605123478032L, -373054737976959636L, -7150688238876681629L, -4326674280168464132L, -796656831783192261L, + -7415439547505577019L, -4657613415954583370L, -1210330751515841308L, -7673985747338482674L, -4980796165745715438L, + -1614309188754756393L, -7926472270612804602L, -5296404319838617848L, -2008819381370884406L, -8173041140997884610L, + -5604615407819967859L, -2394083241347571919L, -8413831053483314306L, -5905602798426754978L, -2770317479606055818L, + -8648977452394866743L, -6199535797066195524L, -3137733727905356501L, -8878612607581929669L, -6486579741050024183L, + -3496538657885142324L, -9102865688819295809L, -6766896092596731857L, -3846934097318526917L, -196981603220770742L, + -7040642529654063570L, -4189117143640191558L, -624710411122851544L, -7307973034592864071L, -4523280274813692185L, + -1042414325089727327L, -7569037980822161435L, -4849611457600313890L, -1450328303573004458L, -7823984217374209643L, + -5168294253290374149L, -1848681798185579782L, -8072955151507069220L, -5479507920956448621L, -2237698882768172872L, + -8316090829371189901L, -5783427518286599473L, -2617598379430861437L, -8553528014785370254L, -6080224000054324913L, + -2988593981640518238L, -8785400266166405755L, -6370064314280619289L, -3350894374423386208L, -9011838011655698236L, + -6653111496142234891L, -3704703351750405709L, -19193171260619233L, -6929524759678968877L, -4050219931171323192L, + -451088895536766085L, -7199459587351560659L, -4387638465762062920L, -872862063775190746L, -7463067817500576073L, + -4717148753448332187L, -1284749923383027329L, -7720497729755473937L, -5038936143766954517L, -1686984161281305242L, + -7971894128441897632L, -5353181642124984136L, -2079791034228842266L, -8217398424034108273L, -5660062011615247437L, + -2463391496091671392L, -8457148712698376476L, -5959749872445582691L, -2838001322129590460L, -8691279853972075893L, + -6252413799037706963L, -3203831230369745799L, -8919923546622172981L, -6538218414850328322L, -3561087000135522498L, + -9143208402725783417L, -6817324484979841368L, -3909969587797413806L, -275775966319379353L, -7089889006590693952L, + -4250675239810979535L, -701658031336336515L, -7356065297226292178L, -4583395603105477319L, -1117558485454458744L, + -7616003081050118571L, -4908317832885260310L, -1523711272679187483L, -7869848573065574033L, -5225624697904579637L, + -1920344853953336643L, -8117744561361917258L, -5535494683275008668L, -2307682335666372931L, -8359830487432564938L, + -5838102090863318269L, -2685941595151759932L, -8596242524610931813L, -6133617137336276863L, -3055335403242958174L, + -8827113654667930715L, -6422206049907525490L, -3416071543957018958L, -9052573742614218705L, -6704031159840385477L, + -3768352931373093942L, -98755145788979524L, -6979250993759194058L, -4112377723771604669L, -528786136287117932L, + -7248020362820530564L, -4448339435098275301L, -948738275445456222L, -7510490449794491995L, -4776427043815727089L, + -1358847786342270957L, -7766808894105001205L, -5096825099203863602L, -1759345355577441598L, -8017119874876982855L, + -5409713825168840664L, -2150456263033662926L, -8261564192037121185L, -5715269221619013577L, -2532400508596379068L, + -8500279345513818773L, -6013663163464885563L, -2905392935903719049L, -8733399612580906262L, -6305063497298744923L, + -3269643353196043250L, -8961056123388608887L, -6589634135808373205L, -3625356651333078602L, -9183376934724255983L, + -6867535149977932074L, -3972732919045027189L, -354230130378896082L, -7138922859127891907L, -4311967555482476980L, + -778273425925708321L, -7403949918844649557L, -4643251380128424042L, -1192378206733142148L, -7662765406849295699L, + -4966770740134231719L, -1596777406740401745L, -7915514906853832947L, -5282707615139903279L, -1991698500497491195L, + -8162340590452013853L, -5591239719637629412L, -2377363631119648861L, -8403381297090862394L, -5892540602936190089L, + -2753989735242849707L, -8638772612167862923L, -6186779746782440750L, -3121788665050663033L, -8868646943297746252L, + -6474122660694794911L, -3480967307441105734L, -9093133594791772940L, -6754730975062328271L, -3831727700400522434L, + -177973607073265139L, -7028762532061872568L, -4174267146649952806L, -606147914885053103L, -7296371474444240046L, + -4508778324627912153L, -1024286887357502287L, -7557708332239520786L, -4835449396872013078L, -1432625727662628443L, + -7812920107430224633L, -5154464115860392887L, -1831394126398103205L, -8062150356639896359L, -5466001927372482545L, + -2220816390788215277L, -8305539271883716405L, -5770238071427257602L, -2601111570856684098L, -8543223759426509417L, + -6067343680855748868L, -2972493582642298180L, -8775337516792518219L, -6357485877563259869L, -3335171328526686933L, + -9002011107970261189L, -6640827866535438582L, -3689348814741910324L, -9223372036854775808L, -6917529027641081856L, + -4035225266123964416L, -432345564227567616L, -7187745005283311616L, -4372995238176751616L, -854558029293551616L, + -7451627795949551616L, -4702848726509551616L, -1266874889709551616L, -7709325833709551616L, -5024971273709551616L, + -1669528073709551616L, -7960984073709551616L, -5339544073709551616L, -2062744073709551616L, -8206744073709551616L, + -5646744073709551616L, -2446744073709551616L, -8446744073709551616L, -5946744073709551616L, -2821744073709551616L, + -8681119073709551616L, -6239712823709551616L, -3187955011209551616L, -8910000909647051616L, -6525815118631426616L, + -3545582879861895366L, -9133518327554766460L, -6805211891016070171L, -3894828845342699810L, -256850038250986858L, + -7078060301547948643L, -4235889358507547899L, -683175679707046970L, -7344513827457986212L, -4568956265895094861L, + -1099509313941480672L, -7604722348854507276L, -4894216917640746191L, -1506085128623544835L, -7858832233030797378L, + -5211854272861108819L, -1903131822648998119L, -8106986416796705681L, -5522047002568494197L, -2290872734783229842L, + -8349324486880600507L, -5824969590173362730L, -2669525969289315508L, -8585982758446904049L, -6120792429631242157L, + -3039304518611664792L, -8817094351773372351L, -6409681921289327535L, -3400416383184271515L, -9042789267131251553L, + -6691800565486676537L, -3753064688430957767L, -79644842111309304L, -6967307053960650171L, -4097447799023424810L, + -510123730351893109L, -7236356359111015049L, -4433759430461380907L, -930513269649338230L, -7499099821171918250L, + -4762188758037509908L, -1341049929119499481L, -7755685233340769032L, -5082920523248573386L, -1741964635633328828L, + -8006256924911912374L, -5396135137712502563L, -2133482903713240300L, -8250955842461857044L, -5702008784649933400L, + -2515824962385028846L, -8489919629131724885L, -6000713517987268202L, -2889205879056697349L, -8723282702051517699L, + -6292417359137009220L, -3253835680493873621L, -8951176327949752869L, -6577284391509803182L, -3609919470959866074L, + -9173728696990998152L, -6855474852811359786L, -3957657547586811828L, -335385916056126881L, -7127145225176161157L, + -4297245513042813542L, -759870872876129024L, -7392448323188662496L, -4628874385558440216L, -1174406963520662366L, + -7651533379841495835L, -4952730706374481889L, -1579227364540714458L, -7904546130479028392L, -5268996644671397586L, + -1974559787411859078L, -8151628894773493780L, -5577850100039479321L, -2360626606621961247L, -8392920656779807636L, + -5879464802547371641L, -2737644984756826647L, -8628557143114098510L, -6174010410465235234L, -3105826994654156138L, + -8858670899299929442L, -6461652605697523899L, -3465379738694516970L, -9083391364325154962L, -6742553186979055799L, + -3816505465296431844L, -158945813193151901L, -7016870160886801794L, -4159401682681114339L, -587566084924005019L, + -7284757830718584993L, -4494261269970843337L, -1006140569036166268L, -7546366883288685774L, -4821272585683469313L, + -1414904713676948737L, -7801844473689174817L, -5140619573684080617L, -1814088448677712867L, -8051334308064652398L, + -5452481866653427593L, -2203916314889396588L, -8294976724446954723L, -5757034887131305500L, -2584607590486743971L, + -8532908771695296838L, -6054449946191733143L, -2956376414312278525L, -8765264286586255934L, -6344894339805432014L, + -3319431906329402113L, -8992173969096958177L, -6628531442943809817L, -3673978285252374367L, -9213765455923815836L, + -6905520801477381891L, -4020214983419339459L, -413582710846786420L, -7176018221920323369L, -4358336758973016307L, + -836234930288882479L, -7440175859071633406L, -4688533805412153853L, -1248981238337804412L, -7698142301602209614L, + -5010991858575374113L, -1652053804791829737L, -7950062655635975442L, -5325892301117581398L, -2045679357969588844L, + -8196078626372074883L, -5633412264537705700L, -2430079312244744221L, -8436328597794046994L, -5933724728815170839L, + -2805469892591575644L, -8670947710510816634L, -6226998619711132888L, -3172062256211528206L, -8900067937773286985L, + -6513398903789220827L, -3530062611309138130L, -9123818159709293187L, -6793086681209228580L, -3879672333084147821L, + -237904397927796872L, -7066219276345954901L, -4221088077005055722L, -664674077828931749L, -7332950326284164199L, + -4554501889427817345L, -1081441343357383777L, -7593429867239446717L, -4880101315621920492L, -1488440626100012711L, + -7847804418953589800L, -5198069505264599346L, -1885900863153361279L, -8096217067111932656L, -5508585315462527915L, + -2274045625900771990L, -8338807543829064350L, -5811823411358942533L, -2653093245771290262L, -8575712306248138270L, + -6107954364382784934L, -3023256937051093263L, -8807064613298015146L, -6397144748195131028L, -3384744916816525881L, + -9032994600651410532L, -6679557232386875260L, -3737760522056206171L, -60514634142869810L, -6955350673980375487L, + -4082502324048081455L, -491441886632713915L, -7224680206786528053L, -4419164240055772162L, -912269281642327298L, + -7487697328667536418L, -4747935642407032618L, -1323233534581402868L, -7744549986754458649L, -5069001465015685407L, + -1724565812842218855L, -7995382660667468640L, -5382542307406947896L, -2116491865831296966L, -8240336443785642460L, + -5688734536304665171L, -2499232151953443560L, -8479549122611984081L, -5987750384837592197L, -2873001962619602342L, + -8713155254278333320L, -6279758049420528746L, -3238011543348273028L, -8941286242233752499L, -6564921784364802720L, + -3594466212028615495L, -9164070410158966541L, -6843401994271320272L, -3942566474411762436L, -316522074587315140L, + -7115355324258153819L, -4282508136895304370L, -741449152691742558L, -7380934748073420955L, -4614482416664388289L, + -1156417002403097458L, -7640289654143017767L, -4938676049251384305L, -1561659043136842477L, -7893565929601608404L, + -5255271393574622601L, -1957403223540890347L, -8140906042354138323L, -5564446534515285000L, -2343872149716718346L, + -8382449121214030822L, -5866375383090150624L, -2721283210435300376L, -8618331034163144591L, -6161227774276542835L, + -3089848699418290639L, -8848684464777513506L, -6449169562544503978L, -3449775934753242068L, -9073638986861858149L, + -6730362715149934782L, -3801267375510030573L, -139898200960150313L, -7004965403241175802L, -4144520735624081848L, + -568964901102714406L, -7273132090830278360L, -4479729095110460046L, -987975350460687153L, -7535013621679011327L, + -4807081008671376254L, -1397165242411832414L, -7790757304148477115L, -5126760611758208489L, -1796764746270372707L, + -8040506994060064798L, -5438947724147693094L, -2186998636757228463L, -8284403175614349646L, -5743817951090549153L, + -2568086420435798537L, -8522583040413455942L, -6041542782089432023L, -2940242459184402125L, -8755180564631333184L, + -6332289687361778576L, -3303676090774835316L, -8982326584375353929L, -6616222212041804507L, -3658591746624867729L, + -9204148869281624187L, -6893500068174642330L, -4005189066790915008L, -394800315061255856L, -7164279224554366766L, + -4343663012265570553L, -817892746904575288L, -7428711994456441411L, -4674203974643163860L, -1231068949876566920L, + -7686947121313936181L, -4996997883215032323L, -1634561335591402499L, -7939129862385708418L, -5312226309554747619L, + -2028596868516046619L, -8185402070463610993L, -5620066569652125837L + ) +} diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala new file mode 100644 index 000000000..00e3f1f3e --- /dev/null +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -0,0 +1,649 @@ +/* + * Copyright 2019-2022 John A. De Goes and the ZIO Contributors + * + * 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 zio.json.internal + +import scala.util.control.NoStackTrace + +// The underlying implementation uses an exception that has no stack trace for +// the failure case, which is 20x faster than retaining stack traces. Therefore, +// we require no boxing of the results on the happy path. This slows down the +// unhappy path a little bit, but it's still on the same order of magnitude as +// the happy path. +// +// This API should only be used by people who know what they are doing. Note +// that Reader implementations consume one character beyond the number that is +// parsed, because there is no terminator character. +object UnsafeNumbers { + + // should never escape into user code + case object UnsafeNumber + extends Exception("if you see this a dev made a mistake using UnsafeNumbers") + with NoStackTrace + + def byte(num: String): Byte = + byte_(new FastStringReader(num), true) + + def byte_(in: OneCharReader, consume: Boolean): Byte = { + val n = int_(in, consume) + if (n < -128 || n > 127) throw UnsafeNumber + n.toByte + } + + def short(num: String): Short = + short_(new FastStringReader(num), true) + + def short_(in: OneCharReader, consume: Boolean): Short = { + val n = int_(in, consume) + if (n < -32768 || n > 32767) throw UnsafeNumber + n.toShort + } + + def int(num: String): Int = + int_(new FastStringReader(num), true) + + def int_(in: OneCharReader, consume: Boolean): Int = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var accum = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -214748364 || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (negate) accum + else if (accum != -2147483648) -accum + else throw UnsafeNumber + } + + def long(num: String): Long = + long_(new FastStringReader(num), true) + + def long_(in: OneCharReader, consume: Boolean): Long = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var accum = ('0' - current).toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -922337203685477580L || { + accum = (accum << 3) + (accum << 1) + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (negate) accum + else if (accum != -9223372036854775808L) -accum + else throw UnsafeNumber + } + + def bigInteger(num: String, max_bits: Int): java.math.BigInteger = + bigInteger_(new FastStringReader(num), true, max_bits) + + def bigInteger_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigInteger = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var bigM10: java.math.BigInteger = null + var m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + if (negate) m10 = -m10 + return java.math.BigInteger.valueOf(m10) + } + if (negate) bigM10 = bigM10.negate + bigM10 + } + + def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = + bigDecimal_(new FastStringReader(num), true, max_bits) + + def bigDecimal_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigDecimal = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + var bigM10: java.math.BigInteger = null + var m10 = -1L + if ('0' <= current && current <= '9') { + m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var e10 = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + e10 -= 1 + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (m10 < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) e10 += exp + else if (exp != -2147483648) e10 -= exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + if (negate) m10 = -m10 + return java.math.BigDecimal.valueOf(m10, -e10) + } + if (negate) bigM10 = bigM10.negate + new java.math.BigDecimal(bigM10, -e10) + } + + def float(num: String, max_bits: Int): Float = + float_(new FastStringReader(num), true, max_bits) + + def float_(in: OneCharReader, consume: Boolean, max_bits: Int): Float = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + if (current == 'N') { + readAll(in, "aN", consume) + return Float.NaN + } + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current == 'I' || current == '+') { + if (current == '+') { + current = in.readChar().toInt + if (current != 'I') throw UnsafeNumber + } + readAll(in, "nfinity", consume) + return if (negate) Float.NegativeInfinity else Float.PositiveInfinity + } + var digits = 1 // calculate digits for m10 only + var m10 = -1L + var bigM10: java.math.BigInteger = null + if ('0' <= current && current <= '9') { + m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var e10 = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + e10 -= 1 + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (m10 < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) e10 += exp + else if (exp != -2147483648) e10 -= exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + var x: Float = + if (e10 == 0) m10.toFloat + else { + if (m10 < 4294967296L && e10 >= digits - 23 && e10 <= 19 - digits) { + val pow10 = pow10Doubles + (if (e10 < 0) m10 / pow10(-e10) + else m10 * pow10(e10)).toFloat + } else toFloat(m10, e10) + } + if (negate) x = -x + return x + } + if (negate) bigM10 = bigM10.negate + new java.math.BigDecimal(bigM10, -e10).floatValue() + } + + // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical + // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct + private[this] def toFloat(m10: Long, e10: Int): Float = + if (m10 == 0 || e10 < -64) 0.0f + else if (e10 >= 39) Float.PositiveInfinity + else { + var shift = java.lang.Long.numberOfLeadingZeros(m10) + var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 + shift = java.lang.Long.numberOfLeadingZeros(m2) + m2 <<= shift + e2 -= shift + val truncatedBitNum = Math.max(-149 - e2, 40) + val savedBitNum = 64 - truncatedBitNum + val mask = -1L >>> Math.max(savedBitNum, 0) + val halfwayDiff = (m2 & mask) - (mask >>> 1) + if (Math.abs(halfwayDiff) > 1 || savedBitNum <= 0) java.lang.Float.intBitsToFloat { + var mf = 0 + if (savedBitNum > 0) mf = (m2 >>> truncatedBitNum).toInt + e2 += truncatedBitNum + if (savedBitNum >= 0 && halfwayDiff > 0) { + if (mf == 0xffffff) { + mf = 0x800000 + e2 += 1 + } else mf += 1 + } + if (e2 == -149) mf + else if (e2 >= 105) 0x7f800000 + else e2 + 150 << 23 | mf & 0x7fffff + } + else java.math.BigDecimal.valueOf(m10, -e10).floatValue() + } + + def double(num: String, max_bits: Int): Double = + double_(new FastStringReader(num), true, max_bits) + + def double_(in: OneCharReader, consume: Boolean, max_bits: Int): Double = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + if (current == 'N') { + readAll(in, "aN", consume) + return Double.NaN + } + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current == 'I' || current == '+') { + if (current == '+') { + current = in.readChar().toInt + if (current != 'I') throw UnsafeNumber + } + readAll(in, "nfinity", consume) + return if (negate) Double.NegativeInfinity else Double.PositiveInfinity + } + var digits = 1 // calculate digits for m10 only + var m10 = -1L + var bigM10: java.math.BigInteger = null + if ('0' <= current && current <= '9') { + m10 = (current - '0').toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + var e10 = 0 + if (current == '.') { + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + e10 -= 1 + if (m10 < 922337203685477580L) { + if (m10 <= 0) m10 = (current - '0').toLong + else { + m10 = (m10 << 3) + (m10 << 1) + (current - '0') + digits += 1 + } + } else { + if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) + bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) + if (bigM10.bitLength >= max_bits) throw UnsafeNumber + } + } + } + if (m10 < 0) throw UnsafeNumber + if ((current | 0x20) == 'e') { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + if (current < '0' || current > '9') throw UnsafeNumber + var exp = '0' - current + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + if (negateExp) e10 += exp + else if (exp != -2147483648) e10 -= exp + else throw UnsafeNumber + } + if (consume && current != -1) throw UnsafeNumber + if (bigM10 eq null) { + var x: Double = + if (e10 == 0) m10.toDouble + else { + if (m10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - digits) { + val pow10 = pow10Doubles + if (e10 < 0) m10 / pow10(-e10) + else if (e10 <= 22) m10 * pow10(e10) + else { + val slop = 16 - digits + (m10 * pow10(slop)) * pow10(e10 - slop) + } + } else toDouble(m10, e10) + } + if (negate) x = -x + return x + } + if (negate) bigM10 = bigM10.negate + new java.math.BigDecimal(bigM10, -e10).doubleValue() + } + + // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical + // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct + private[this] def toDouble(m10: Long, e10: Int): Double = + if (m10 == 0 || e10 < -343) 0.0 + else if (e10 >= 310) Double.PositiveInfinity + else { + var shift = java.lang.Long.numberOfLeadingZeros(m10) + var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 + shift = java.lang.Long.numberOfLeadingZeros(m2) + m2 <<= shift + e2 -= shift + val truncatedBitNum = Math.max(-1074 - e2, 11) + val savedBitNum = 64 - truncatedBitNum + val mask = -1L >>> Math.max(savedBitNum, 0) + val halfwayDiff = (m2 & mask) - (mask >>> 1) + if (Math.abs(halfwayDiff) > 1 || savedBitNum <= 0) java.lang.Double.longBitsToDouble { + if (savedBitNum <= 0) m2 = 0 + m2 >>>= truncatedBitNum + e2 += truncatedBitNum + if (savedBitNum >= 0 && halfwayDiff > 0) { + if (m2 == 0x1fffffffffffffL) { + m2 = 0x10000000000000L + e2 += 1 + } else m2 += 1 + } + if (e2 == -1074) m2 + else if (e2 >= 972) 0x7ff0000000000000L + else (e2 + 1075).toLong << 52 | m2 & 0xfffffffffffffL + } + else java.math.BigDecimal.valueOf(m10, -e10).doubleValue() + } + + @noinline private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { + val len = s.length + var i = 0 + while (i < len) { + if (in.readChar() != s.charAt(i)) throw UnsafeNumber + i += 1 + } + val current = in.read() // to be consistent read the terminator + if (consume && current != -1) throw UnsafeNumber + } + + @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = + Math.multiplyHigh(x, y) + x + y // Use implementation that works only when both params are negative + + private[this] final val bigIntegers: Array[java.math.BigInteger] = + (0L to 9L).map(java.math.BigInteger.valueOf).toArray + + private[this] final val pow10Doubles: Array[Double] = + Array(1, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, + 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22) + + private[this] final val pow10Mantissas: Array[Long] = Array( + -4671960508600951122L, -1228264617323800998L, -7685194413468457480L, -4994806998408183946L, -1631822729582842029L, + -7937418233630358124L, -5310086773610559751L, -2025922448585811785L, -8183730558007214222L, -5617977179081629873L, + -2410785455424649437L, -8424269937281487754L, -5918651403174471789L, -2786628235540701832L, -8659171674854020501L, + -6212278575140137722L, -3153662200497784248L, -8888567902952197011L, -6499023860262858360L, -3512093806901185046L, + -9112587656954322510L, -6779048552765515233L, -3862124672529506138L, -215969822234494768L, -7052510166537641086L, + -4203951689744663454L, -643253593753441413L, -7319562523736982739L, -4537767136243840520L, -1060522901877412746L, + -7580355841314464822L, -4863758783215693124L, -1468012460592228501L, -7835036815511224669L, -5182110000961642932L, + -1865951482774665761L, -8083748704375247957L, -5492999862041672042L, -2254563809124702148L, -8326631408344020699L, + -5796603242002637969L, -2634068034075909558L, -8563821548938525330L, -6093090917745768758L, -3004677628754823043L, + -8795452545612846258L, -6382629663588669919L, -3366601061058449494L, -9021654690802612790L, -6665382345075878084L, + -3720041912917459700L, -38366372719436721L, -6941508010590729807L, -4065198994811024355L, -469812725086392539L, + -7211161980820077193L, -4402266457597708587L, -891147053569747830L, -7474495936122174250L, -4731433901725329908L, + -1302606358729274481L, -7731658001846878407L, -5052886483881210105L, -1704422086424124727L, -7982792831656159810L, + -5366805021142811859L, -2096820258001126919L, -8228041688891786181L, -5673366092687344822L, -2480021597431793123L, + -8467542526035952558L, -5972742139117552794L, -2854241655469553088L, -8701430062309552536L, -6265101559459552766L, + -3219690930897053053L, -8929835859451740015L, -6550608805887287114L, -3576574988931720989L, -9152888395723407474L, + -6829424476226871438L, -3925094576856201394L, -294682202642863838L, -7101705404292871755L, -4265445736938701790L, + -720121152745989333L, -7367604748107325189L, -4597819916706768583L, -1135588877456072824L, -7627272076051127371L, + -4922404076636521310L, -1541319077368263733L, -7880853450996246689L, -5239380795317920458L, -1937539975720012668L, + -8128491512466089774L, -5548928372155224313L, -2324474446766642487L, -8370325556870233411L, -5851220927660403859L, + -2702340141148116920L, -8606491615858654931L, -6146428501395930760L, -3071349608317525546L, -8837122532839535322L, + -6434717147622031249L, -3431710416100151157L, -9062348037703676329L, -6716249028702207507L, -3783625267450371480L, + -117845565885576446L, -6991182506319567135L, -4127292114472071014L, -547429124662700864L, -7259672230555269896L, + -4462904269766699466L, -966944318780986428L, -7521869226879198374L, -4790650515171610063L, -1376627125537124675L, + -7777920981101784778L, -5110715207949843068L, -1776707991509915931L, -8027971522334779313L, -5423278384491086237L, + -2167411962186469893L, -8272161504007625539L, -5728515861582144020L, -2548958808550292121L, -8510628282985014432L, + -6026599335303880135L, -2921563150702462265L, -8743505996830120772L, -6317696477610263061L, -3285434578585440922L, + -8970925639256982432L, -6601971030643840136L, -3640777769877412266L, -9193015133814464522L, -6879582898840692749L, + -3987792605123478032L, -373054737976959636L, -7150688238876681629L, -4326674280168464132L, -796656831783192261L, + -7415439547505577019L, -4657613415954583370L, -1210330751515841308L, -7673985747338482674L, -4980796165745715438L, + -1614309188754756393L, -7926472270612804602L, -5296404319838617848L, -2008819381370884406L, -8173041140997884610L, + -5604615407819967859L, -2394083241347571919L, -8413831053483314306L, -5905602798426754978L, -2770317479606055818L, + -8648977452394866743L, -6199535797066195524L, -3137733727905356501L, -8878612607581929669L, -6486579741050024183L, + -3496538657885142324L, -9102865688819295809L, -6766896092596731857L, -3846934097318526917L, -196981603220770742L, + -7040642529654063570L, -4189117143640191558L, -624710411122851544L, -7307973034592864071L, -4523280274813692185L, + -1042414325089727327L, -7569037980822161435L, -4849611457600313890L, -1450328303573004458L, -7823984217374209643L, + -5168294253290374149L, -1848681798185579782L, -8072955151507069220L, -5479507920956448621L, -2237698882768172872L, + -8316090829371189901L, -5783427518286599473L, -2617598379430861437L, -8553528014785370254L, -6080224000054324913L, + -2988593981640518238L, -8785400266166405755L, -6370064314280619289L, -3350894374423386208L, -9011838011655698236L, + -6653111496142234891L, -3704703351750405709L, -19193171260619233L, -6929524759678968877L, -4050219931171323192L, + -451088895536766085L, -7199459587351560659L, -4387638465762062920L, -872862063775190746L, -7463067817500576073L, + -4717148753448332187L, -1284749923383027329L, -7720497729755473937L, -5038936143766954517L, -1686984161281305242L, + -7971894128441897632L, -5353181642124984136L, -2079791034228842266L, -8217398424034108273L, -5660062011615247437L, + -2463391496091671392L, -8457148712698376476L, -5959749872445582691L, -2838001322129590460L, -8691279853972075893L, + -6252413799037706963L, -3203831230369745799L, -8919923546622172981L, -6538218414850328322L, -3561087000135522498L, + -9143208402725783417L, -6817324484979841368L, -3909969587797413806L, -275775966319379353L, -7089889006590693952L, + -4250675239810979535L, -701658031336336515L, -7356065297226292178L, -4583395603105477319L, -1117558485454458744L, + -7616003081050118571L, -4908317832885260310L, -1523711272679187483L, -7869848573065574033L, -5225624697904579637L, + -1920344853953336643L, -8117744561361917258L, -5535494683275008668L, -2307682335666372931L, -8359830487432564938L, + -5838102090863318269L, -2685941595151759932L, -8596242524610931813L, -6133617137336276863L, -3055335403242958174L, + -8827113654667930715L, -6422206049907525490L, -3416071543957018958L, -9052573742614218705L, -6704031159840385477L, + -3768352931373093942L, -98755145788979524L, -6979250993759194058L, -4112377723771604669L, -528786136287117932L, + -7248020362820530564L, -4448339435098275301L, -948738275445456222L, -7510490449794491995L, -4776427043815727089L, + -1358847786342270957L, -7766808894105001205L, -5096825099203863602L, -1759345355577441598L, -8017119874876982855L, + -5409713825168840664L, -2150456263033662926L, -8261564192037121185L, -5715269221619013577L, -2532400508596379068L, + -8500279345513818773L, -6013663163464885563L, -2905392935903719049L, -8733399612580906262L, -6305063497298744923L, + -3269643353196043250L, -8961056123388608887L, -6589634135808373205L, -3625356651333078602L, -9183376934724255983L, + -6867535149977932074L, -3972732919045027189L, -354230130378896082L, -7138922859127891907L, -4311967555482476980L, + -778273425925708321L, -7403949918844649557L, -4643251380128424042L, -1192378206733142148L, -7662765406849295699L, + -4966770740134231719L, -1596777406740401745L, -7915514906853832947L, -5282707615139903279L, -1991698500497491195L, + -8162340590452013853L, -5591239719637629412L, -2377363631119648861L, -8403381297090862394L, -5892540602936190089L, + -2753989735242849707L, -8638772612167862923L, -6186779746782440750L, -3121788665050663033L, -8868646943297746252L, + -6474122660694794911L, -3480967307441105734L, -9093133594791772940L, -6754730975062328271L, -3831727700400522434L, + -177973607073265139L, -7028762532061872568L, -4174267146649952806L, -606147914885053103L, -7296371474444240046L, + -4508778324627912153L, -1024286887357502287L, -7557708332239520786L, -4835449396872013078L, -1432625727662628443L, + -7812920107430224633L, -5154464115860392887L, -1831394126398103205L, -8062150356639896359L, -5466001927372482545L, + -2220816390788215277L, -8305539271883716405L, -5770238071427257602L, -2601111570856684098L, -8543223759426509417L, + -6067343680855748868L, -2972493582642298180L, -8775337516792518219L, -6357485877563259869L, -3335171328526686933L, + -9002011107970261189L, -6640827866535438582L, -3689348814741910324L, -9223372036854775808L, -6917529027641081856L, + -4035225266123964416L, -432345564227567616L, -7187745005283311616L, -4372995238176751616L, -854558029293551616L, + -7451627795949551616L, -4702848726509551616L, -1266874889709551616L, -7709325833709551616L, -5024971273709551616L, + -1669528073709551616L, -7960984073709551616L, -5339544073709551616L, -2062744073709551616L, -8206744073709551616L, + -5646744073709551616L, -2446744073709551616L, -8446744073709551616L, -5946744073709551616L, -2821744073709551616L, + -8681119073709551616L, -6239712823709551616L, -3187955011209551616L, -8910000909647051616L, -6525815118631426616L, + -3545582879861895366L, -9133518327554766460L, -6805211891016070171L, -3894828845342699810L, -256850038250986858L, + -7078060301547948643L, -4235889358507547899L, -683175679707046970L, -7344513827457986212L, -4568956265895094861L, + -1099509313941480672L, -7604722348854507276L, -4894216917640746191L, -1506085128623544835L, -7858832233030797378L, + -5211854272861108819L, -1903131822648998119L, -8106986416796705681L, -5522047002568494197L, -2290872734783229842L, + -8349324486880600507L, -5824969590173362730L, -2669525969289315508L, -8585982758446904049L, -6120792429631242157L, + -3039304518611664792L, -8817094351773372351L, -6409681921289327535L, -3400416383184271515L, -9042789267131251553L, + -6691800565486676537L, -3753064688430957767L, -79644842111309304L, -6967307053960650171L, -4097447799023424810L, + -510123730351893109L, -7236356359111015049L, -4433759430461380907L, -930513269649338230L, -7499099821171918250L, + -4762188758037509908L, -1341049929119499481L, -7755685233340769032L, -5082920523248573386L, -1741964635633328828L, + -8006256924911912374L, -5396135137712502563L, -2133482903713240300L, -8250955842461857044L, -5702008784649933400L, + -2515824962385028846L, -8489919629131724885L, -6000713517987268202L, -2889205879056697349L, -8723282702051517699L, + -6292417359137009220L, -3253835680493873621L, -8951176327949752869L, -6577284391509803182L, -3609919470959866074L, + -9173728696990998152L, -6855474852811359786L, -3957657547586811828L, -335385916056126881L, -7127145225176161157L, + -4297245513042813542L, -759870872876129024L, -7392448323188662496L, -4628874385558440216L, -1174406963520662366L, + -7651533379841495835L, -4952730706374481889L, -1579227364540714458L, -7904546130479028392L, -5268996644671397586L, + -1974559787411859078L, -8151628894773493780L, -5577850100039479321L, -2360626606621961247L, -8392920656779807636L, + -5879464802547371641L, -2737644984756826647L, -8628557143114098510L, -6174010410465235234L, -3105826994654156138L, + -8858670899299929442L, -6461652605697523899L, -3465379738694516970L, -9083391364325154962L, -6742553186979055799L, + -3816505465296431844L, -158945813193151901L, -7016870160886801794L, -4159401682681114339L, -587566084924005019L, + -7284757830718584993L, -4494261269970843337L, -1006140569036166268L, -7546366883288685774L, -4821272585683469313L, + -1414904713676948737L, -7801844473689174817L, -5140619573684080617L, -1814088448677712867L, -8051334308064652398L, + -5452481866653427593L, -2203916314889396588L, -8294976724446954723L, -5757034887131305500L, -2584607590486743971L, + -8532908771695296838L, -6054449946191733143L, -2956376414312278525L, -8765264286586255934L, -6344894339805432014L, + -3319431906329402113L, -8992173969096958177L, -6628531442943809817L, -3673978285252374367L, -9213765455923815836L, + -6905520801477381891L, -4020214983419339459L, -413582710846786420L, -7176018221920323369L, -4358336758973016307L, + -836234930288882479L, -7440175859071633406L, -4688533805412153853L, -1248981238337804412L, -7698142301602209614L, + -5010991858575374113L, -1652053804791829737L, -7950062655635975442L, -5325892301117581398L, -2045679357969588844L, + -8196078626372074883L, -5633412264537705700L, -2430079312244744221L, -8436328597794046994L, -5933724728815170839L, + -2805469892591575644L, -8670947710510816634L, -6226998619711132888L, -3172062256211528206L, -8900067937773286985L, + -6513398903789220827L, -3530062611309138130L, -9123818159709293187L, -6793086681209228580L, -3879672333084147821L, + -237904397927796872L, -7066219276345954901L, -4221088077005055722L, -664674077828931749L, -7332950326284164199L, + -4554501889427817345L, -1081441343357383777L, -7593429867239446717L, -4880101315621920492L, -1488440626100012711L, + -7847804418953589800L, -5198069505264599346L, -1885900863153361279L, -8096217067111932656L, -5508585315462527915L, + -2274045625900771990L, -8338807543829064350L, -5811823411358942533L, -2653093245771290262L, -8575712306248138270L, + -6107954364382784934L, -3023256937051093263L, -8807064613298015146L, -6397144748195131028L, -3384744916816525881L, + -9032994600651410532L, -6679557232386875260L, -3737760522056206171L, -60514634142869810L, -6955350673980375487L, + -4082502324048081455L, -491441886632713915L, -7224680206786528053L, -4419164240055772162L, -912269281642327298L, + -7487697328667536418L, -4747935642407032618L, -1323233534581402868L, -7744549986754458649L, -5069001465015685407L, + -1724565812842218855L, -7995382660667468640L, -5382542307406947896L, -2116491865831296966L, -8240336443785642460L, + -5688734536304665171L, -2499232151953443560L, -8479549122611984081L, -5987750384837592197L, -2873001962619602342L, + -8713155254278333320L, -6279758049420528746L, -3238011543348273028L, -8941286242233752499L, -6564921784364802720L, + -3594466212028615495L, -9164070410158966541L, -6843401994271320272L, -3942566474411762436L, -316522074587315140L, + -7115355324258153819L, -4282508136895304370L, -741449152691742558L, -7380934748073420955L, -4614482416664388289L, + -1156417002403097458L, -7640289654143017767L, -4938676049251384305L, -1561659043136842477L, -7893565929601608404L, + -5255271393574622601L, -1957403223540890347L, -8140906042354138323L, -5564446534515285000L, -2343872149716718346L, + -8382449121214030822L, -5866375383090150624L, -2721283210435300376L, -8618331034163144591L, -6161227774276542835L, + -3089848699418290639L, -8848684464777513506L, -6449169562544503978L, -3449775934753242068L, -9073638986861858149L, + -6730362715149934782L, -3801267375510030573L, -139898200960150313L, -7004965403241175802L, -4144520735624081848L, + -568964901102714406L, -7273132090830278360L, -4479729095110460046L, -987975350460687153L, -7535013621679011327L, + -4807081008671376254L, -1397165242411832414L, -7790757304148477115L, -5126760611758208489L, -1796764746270372707L, + -8040506994060064798L, -5438947724147693094L, -2186998636757228463L, -8284403175614349646L, -5743817951090549153L, + -2568086420435798537L, -8522583040413455942L, -6041542782089432023L, -2940242459184402125L, -8755180564631333184L, + -6332289687361778576L, -3303676090774835316L, -8982326584375353929L, -6616222212041804507L, -3658591746624867729L, + -9204148869281624187L, -6893500068174642330L, -4005189066790915008L, -394800315061255856L, -7164279224554366766L, + -4343663012265570553L, -817892746904575288L, -7428711994456441411L, -4674203974643163860L, -1231068949876566920L, + -7686947121313936181L, -4996997883215032323L, -1634561335591402499L, -7939129862385708418L, -5312226309554747619L, + -2028596868516046619L, -8185402070463610993L, -5620066569652125837L + ) +} diff --git a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala index 1a7c5f1c3..84eceb3b2 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/numbers.scala @@ -15,8 +15,6 @@ */ package zio.json.internal -import scala.util.control.NoStackTrace - // specialised Options to avoid boxing. Prefer .isEmpty guarded access to .value // for higher performance: pattern matching is slightly slower. @@ -103,391 +101,3 @@ case object DoubleNone extends DoubleOption { case class DoubleSome(value: Double) extends DoubleOption { def isEmpty = false } - -// The underlying implementation uses an exception that has no stack trace for -// the failure case, which is 20x faster than retaining stack traces. Therefore, -// we require no boxing of the results on the happy path. This slows down the -// unhappy path a little bit, but it's still on the same order of magnitude as -// the happy path. -// -// This API should only be used by people who know what they are doing. Note -// that Reader implementations consume one character beyond the number that is -// parsed, because there is no terminator character. -object UnsafeNumbers { - - // should never escape into user code - case object UnsafeNumber - extends Exception( - "if you see this a dev made a mistake using UnsafeNumbers" - ) - with NoStackTrace - - def byte(num: String): Byte = - byte_(new FastStringReader(num), true) - - def byte_(in: OneCharReader, consume: Boolean): Byte = { - val n = int_(in, consume) - if (n < -128 || n > 127) throw UnsafeNumber - n.toByte - } - - def short(num: String): Short = - short_(new FastStringReader(num), true) - - def short_(in: OneCharReader, consume: Boolean): Short = { - val n = int_(in, consume) - if (n < -32768 || n > 32767) throw UnsafeNumber - n.toShort - } - - def int(num: String): Int = - int_(new FastStringReader(num), true) - - def int_(in: OneCharReader, consume: Boolean): Int = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var accum = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - accum < -214748364 || { - accum = accum * 10 + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (negate) accum - else if (accum != -2147483648) -accum - else throw UnsafeNumber - } - - def long(num: String): Long = - long_(new FastStringReader(num), true) - - def long_(in: OneCharReader, consume: Boolean): Long = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var accum = ('0' - current).toLong - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - accum < -922337203685477580L || { - accum = (accum << 3) + (accum << 1) + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (negate) accum - else if (accum != -9223372036854775808L) -accum - else throw UnsafeNumber - } - - def bigInteger(num: String, max_bits: Int): java.math.BigInteger = - bigInteger_(new FastStringReader(num), true, max_bits) - - def bigInteger_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigInteger = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var bigSig: java.math.BigInteger = null - var sig = (current - '0').toLong - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if (sig < 922337203685477580L) sig = (sig << 3) + (sig << 1) + (current - '0') - else { - if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) - bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigSig.bitLength >= max_bits) throw UnsafeNumber - } - } - if (consume && current != -1) throw UnsafeNumber - if (bigSig eq null) { - if (negate) sig = -sig - return java.math.BigInteger.valueOf(sig) - } - if (negate) bigSig = bigSig.negate - bigSig - } - - def float(num: String, max_bits: Int): Float = - float_(new FastStringReader(num), true, max_bits) - - def float_(in: OneCharReader, consume: Boolean, max_bits: Int): Float = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - if (current == 'N') { - readAll(in, "aN", consume) - return Float.NaN - } - val negate = current == '-' - if (negate) current = in.readChar().toInt - if (current == 'I' || current == '+') { - if (current == '+') { - current = in.readChar().toInt - if (current != 'I') throw UnsafeNumber - } - readAll(in, "nfinity", consume) - return if (negate) Float.NegativeInfinity else Float.PositiveInfinity - } - var sig = -1L - var bigSig: java.math.BigInteger = null - if ('0' <= current && current <= '9') { - sig = (current - '0').toLong - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if (sig < 922337203685477580L) sig = (sig << 3) + (sig << 1) + (current - '0') - else { - if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) - bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigSig.bitLength >= max_bits) throw UnsafeNumber - } - } - } - var scale, exp = 0 - if (current == '.') { - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - scale += 1 - if (sig < 922337203685477580L) { - if (sig < 0) sig = (current - '0').toLong - else sig = (sig << 3) + (sig << 1) + (current - '0') - } else { - if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) - bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigSig.bitLength >= max_bits) throw UnsafeNumber - } - } - } - if (sig < 0) throw UnsafeNumber - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 - } - ) throw UnsafeNumber - } - if (negateExp) {} - else if (exp != -2147483648) exp = -exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (sig == 0) { - return if (negate) -0.0f else 0.0f - } else if (bigSig eq null) { - if (negate) sig = -sig - return java.math.BigDecimal.valueOf(sig, scale - exp).floatValue() - } - if (negate) bigSig = bigSig.negate - new java.math.BigDecimal(bigSig, scale - exp).floatValue() - } - - def double(num: String, max_bits: Int): Double = - double_(new FastStringReader(num), true, max_bits) - - def double_(in: OneCharReader, consume: Boolean, max_bits: Int): Double = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - if (current == 'N') { - readAll(in, "aN", consume) - return Double.NaN - } - val negate = current == '-' - if (negate) current = in.readChar().toInt - if (current == 'I' || current == '+') { - if (current == '+') { - current = in.readChar().toInt - if (current != 'I') throw UnsafeNumber - } - readAll(in, "nfinity", consume) - return if (negate) Double.NegativeInfinity else Double.PositiveInfinity - } - var sig = -1L - var bigSig: java.math.BigInteger = null - if ('0' <= current && current <= '9') { - sig = (current - '0').toLong - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if (sig < 922337203685477580L) sig = (sig << 3) + (sig << 1) + (current - '0') - else { - if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) - bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigSig.bitLength >= max_bits) throw UnsafeNumber - } - } - } - var scale, exp = 0 - if (current == '.') { - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - scale += 1 - if (sig < 922337203685477580L) { - if (sig < 0) sig = (current - '0').toLong - else sig = (sig << 3) + (sig << 1) + (current - '0') - } else { - if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) - bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigSig.bitLength >= max_bits) throw UnsafeNumber - } - } - } - if (sig < 0) throw UnsafeNumber - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 - } - ) throw UnsafeNumber - } - if (negateExp) {} - else if (exp != -2147483648) exp = -exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (sig == 0) { - return if (negate) -0.0 else 0.0 - } else if (bigSig eq null) { - if (negate) sig = -sig - return java.math.BigDecimal.valueOf(sig, scale - exp).doubleValue() - } - if (negate) bigSig = bigSig.negate - new java.math.BigDecimal(bigSig, scale - exp).doubleValue() - } - - def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = - bigDecimal_(new FastStringReader(num), true, max_bits) - - def bigDecimal_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigDecimal = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - var bigSig: java.math.BigInteger = null - var sig = -1L - if ('0' <= current && current <= '9') { - sig = (current - '0').toLong - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if (sig < 922337203685477580L) sig = (sig << 3) + (sig << 1) + (current - '0') - else { - if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) - bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigSig.bitLength >= max_bits) throw UnsafeNumber - } - } - } - var scale, exp = 0 - if (current == '.') { - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - scale += 1 - if (sig < 922337203685477580L) { - if (sig < 0) sig = (current - '0').toLong - else sig = (sig << 3) + (sig << 1) + (current - '0') - } else { - if (bigSig eq null) bigSig = java.math.BigInteger.valueOf(sig) - bigSig = bigSig.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigSig.bitLength >= max_bits) throw UnsafeNumber - } - } - } - if (sig < 0) throw UnsafeNumber - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 - } - ) throw UnsafeNumber - } - if (negateExp) {} - else if (exp != -2147483648) exp = -exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (bigSig eq null) { - if (negate) sig = -sig - return java.math.BigDecimal.valueOf(sig, scale - exp) - } - if (negate) bigSig = bigSig.negate - new java.math.BigDecimal(bigSig, scale - exp) - } - - @noinline - private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { - val len = s.length - var i = 0 - while (i < len) { - if (in.readChar() != s.charAt(i)) throw UnsafeNumber - i += 1 - } - val current = in.read() // to be consistent read the terminator - if (consume && current != -1) throw UnsafeNumber - } - - // note that bigDecimal does not have a negative zero - private[this] val bigIntegers: Array[java.math.BigInteger] = - (0L to 9L).map(java.math.BigInteger.valueOf).toArray -} diff --git a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala index b0a4f93e5..149668481 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala @@ -8,88 +8,99 @@ import zio.test._ object SafeNumbersSpec extends ZIOSpecDefault { val spec = suite("SafeNumbers")( - test("valid big decimals") { - check(genBigDecimal)(i => assert(SafeNumbers.bigDecimal(i.toString, 2048))(isSome(equalTo(i)))) - }, - test("invalid big decimals") { - val invalidBigDecimalEdgeCases = List( - "N", - "Inf", - "-NaN", - "+NaN", - "e1", - "1.1.1", - "1 ", - "NaN", - "Infinity", - "+Infinity", - "-Infinity" - ).map(s => SafeNumbers.bigDecimal(s)) + suite("BigDecimal")( + test("valid big decimals") { + check(genBigDecimal)(i => assert(SafeNumbers.bigDecimal(i.toString, 2048))(isSome(equalTo(i)))) + }, + test("invalid big decimals") { + val invalidBigDecimalEdgeCases = List( + "N", + "Inf", + "-NaN", + "+NaN", + "e1", + "1.1.1", + "1 ", + "NaN", + "Infinity", + "+Infinity", + "-Infinity", + "1eO", + "1e+2147483648", + "1e+3147483648", + "9" * 99, + "0." + "9" * 99 + ).map(s => SafeNumbers.bigDecimal(s)) - assert(invalidBigDecimalEdgeCases)(forall(isNone)) - }, - test("valid big decimal edge cases") { - val invalidBigDecimalEdgeCases = List( - ".0", - "-.0", - "0", - "0.0", - "-0.0", // zeroes - "0000.1", - "0.00001", - "000.00001000" // various trailing zeros, should be preserved - ) + assert(invalidBigDecimalEdgeCases)(forall(isNone)) + }, + test("valid big decimal edge cases") { + val invalidBigDecimalEdgeCases = List( + ".0", + "-.0", + "0", + "0.0", + "-0.0", // zeroes + "0000.1", + "0.00001", + "000.00001000" // various trailing zeros, should be preserved + ) - check(Gen.fromIterable(invalidBigDecimalEdgeCases)) { s => - assert(SafeNumbers.bigDecimal(s).get.compareTo(new java.math.BigDecimal(s)))(equalTo(0)) + check(Gen.fromIterable(invalidBigDecimalEdgeCases)) { s => + assert(SafeNumbers.bigDecimal(s).get.compareTo(new java.math.BigDecimal(s)))(equalTo(0)) + } + }, + test("invalid BigDecimal text") { + check(genAlphaLowerString)(s => assert(SafeNumbers.bigDecimal(s))(isNone)) } - }, - test("invalid BigDecimal text") { - check(genAlphaLowerString)(s => assert(SafeNumbers.bigDecimal(s))(isNone)) - }, - test("valid BigInteger edge cases") { - val inputs = List( - "00", - "01", - "0000001", - "-9223372036854775807", - "9223372036854775806", - "-9223372036854775809", - "9223372036854775808" - ) + ), + suite("BigInteger")( + test("valid BigInteger edge cases") { + val inputs = List( + "0", + "0123", + "-123", + "-9223372036854775807", + "9223372036854775806", + "-9223372036854775809", + "9223372036854775808" + ) - check(Gen.fromIterable(inputs)) { s => - assert(SafeNumbers.bigInteger(s))( - isSome( - equalTo(new java.math.BigInteger(s)) + check(Gen.fromIterable(inputs)) { s => + assert(SafeNumbers.bigInteger(s))( + isSome( + equalTo(new java.math.BigInteger(s)) + ) ) - ) - } - }, - test("invalid BigInteger edge cases") { - val inputs = List("0foo", "01foo", "0.1", "", "1 ") + } + }, + test("invalid BigInteger edge cases") { + val inputs = List("0e+1", "01E-1", "0.1", "", "1 ") - check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInteger(s))(isNone)) - }, - test("valid big Integer") { - check(genBigInteger)(i => assert(SafeNumbers.bigInteger(i.toString, 2048))(isSome(equalTo(i)))) - }, - test("invalid BigInteger") { - check(genAlphaLowerString)(s => assert(SafeNumbers.bigInteger(s))(isNone)) - }, - test("valid Byte") { - check(Gen.byte(Byte.MinValue, Byte.MaxValue)) { b => - assert(SafeNumbers.byte(b.toString))(equalTo(ByteSome(b))) + check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInteger(s))(isNone)) + }, + test("valid big Integer") { + check(genBigInteger)(i => assert(SafeNumbers.bigInteger(i.toString, 2048))(isSome(equalTo(i)))) + }, + test("invalid BigInteger") { + check(genAlphaLowerString)(s => assert(SafeNumbers.bigInteger(s))(isNone)) } - }, - test("invalid Byte (numbers)") { - check(Gen.long.filter(i => i < Byte.MinValue || i > Byte.MaxValue)) { b => - assert(SafeNumbers.byte(b.toString))(equalTo(ByteNone)) + ), + suite("Byte")( + test("valid Byte") { + check(Gen.byte(Byte.MinValue, Byte.MaxValue)) { b => + assert(SafeNumbers.byte(b.toString))(equalTo(ByteSome(b))) + } + }, + test("invalid Byte (numbers)") { + check(Gen.int.filter(i => i < Byte.MinValue || i > Byte.MaxValue)) { b => + assert(SafeNumbers.byte(b.toString))(equalTo(ByteNone)) + } + }, + test("invalid Byte (text)") { + check(genAlphaLowerString)(b => assert(SafeNumbers.byte(b.toString))(equalTo(ByteNone))) } - }, - test("invalid Byte (text)") { - check(genAlphaLowerString)(b => assert(SafeNumbers.byte(b.toString))(equalTo(ByteNone))) - }, + ), suite("Double")( test("valid") { check(Gen.double.filterNot(_.isNaN)) { d => @@ -102,8 +113,25 @@ object SafeNumbersSpec extends ZIOSpecDefault { test("valid (from Long)") { check(Gen.long)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i.toDouble)))) }, + test("valid (from BigDecimal)") { + check(genBigDecimal)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i.doubleValue())))) + }, test("invalid edge cases") { - val inputs = List("N", "Inf", "-NaN", "+NaN", "e1", "1.1.1", "1 ") + val inputs = List( + "N", + "Inf", + "Info", + "-NaN", + "+NaN", + "e1", + "1.1.1", + "1 ", + "1eO", + "1e+2147483648", + "1e+3147483648", + "9" * 99, + "0." + "9" * 99 + ) check(Gen.fromIterable(inputs))(i => assert(SafeNumbers.double(i))(equalTo(DoubleNone))) }, @@ -116,13 +144,31 @@ object SafeNumbersSpec extends ZIOSpecDefault { "-0.0", // zeroes "0000.1", "0.00001", + "0.0e-12", + "1.1e-12", + "1.1e-1234", + "1.1e+1234", "000.00001000", // trailing zeros "NaN", "92233720368547758070", // overflows a Long significand "Infinity", "+Infinity", "-Infinity", - "3.976210887433566E-281" // rounds if a naive scaling is used + "503599627370496E+13", // fast path + "503599627370496E+23", // fast path with slop + "3.976210887433566E-281", // rounds if a naive scaling is used + "9007199254740993.0", // round-down, halfway + "18014398509481986.0", + "9223372036854776832.0", + "9007199254740995.0", // round-up, halfway + "18014398509481990.0", + "9223372036854778880.0", + "9223372036854776833.0", // round-up, above halfway + "36028797018963967.0", // 2^n - 1 integer regression + "2.2250738585072014E-308", + "2.2250738585072013E-308", + "2.2250738585072012E-308", + "2.2250738585072011E-308" ) check(Gen.fromIterable(inputs)) { s => @@ -157,7 +203,20 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(Gen.long)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.toFloat)))) }, test("invalid edge cases") { - val inputs = List("N", "Inf", "-NaN", "+NaN", "e1", "1.1.1") + val inputs = List( + "N", + "Inf", + "Info", + "-NaN", + "+NaN", + "e1", + "1.1.1", + "1eO", + "1e+2147483648", + "1e+3147483648", + "9" * 99, + "0." + "9" * 99 + ) check(Gen.fromIterable(inputs))(i => assert(SafeNumbers.float(i))(equalTo(FloatNone))) }, @@ -170,12 +229,39 @@ object SafeNumbersSpec extends ZIOSpecDefault { "-0.0", // zeroes "0000.1", "0.00001", + "0.0e-12", + "1.1e-12", + "1.1e-1234", + "1.1e+1234", "000.00001000", // trailing zeros "NaN", "92233720368547758070", // overflows a Long significand "Infinity", "+Infinity", - "-Infinity" + "-Infinity", + "16777217.0", // round-down, halfway + "33554434.0", + "17179870208.0", + "16777219.0", // round-up, halfway + "33554438.0", + "17179872256.0", + "33554435.0", // round-up, above halfway + "17179870209.0", + "37930954282500097", // fast path with `toFloat` + "48696272630054913", + // TODO: uncomment after release of Scala Native 0.5.7 + // "1.00000017881393432617187499", // check exactly halfway, round-up at halfway + // "1.000000178813934326171875", + // "1.00000017881393432617187501", + "36028797018963967.0", // 2^n - 1 integer regression + "1.17549435E-38", + "1.17549434E-38", + "1.17549433E-38", + "1.17549432E-38", + "1.17549431E-38", + "1.17549430E-38", + "1.17549429E-38", + "1.17549428E-38" ) check(Gen.fromIterable(inputs)) { s => @@ -190,6 +276,9 @@ object SafeNumbersSpec extends ZIOSpecDefault { assert(SafeNumbers.float(d.toString))(equalTo(FloatSome(d.toFloat))) } }, + test("valid (from BigDecimal)") { + check(genBigDecimal)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.floatValue())))) + }, test("invalid float (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.float(s))(equalTo(FloatNone))) } @@ -257,7 +346,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(Gen.short)(d => assert(SafeNumbers.short(d.toString))(equalTo(ShortSome(d)))) }, test("invalid (out of range)") { - check(Gen.long.filter(i => i < Short.MinValue || i > Short.MaxValue))(d => + check(Gen.int.filter(i => i < Short.MinValue || i > Short.MaxValue))(d => assert(SafeNumbers.short(d.toString))(equalTo(ShortNone)) ) }, From 5e24fcc2e951d935c84890c95d078b65c7824e1b Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 31 Jan 2025 15:22:16 +0100 Subject: [PATCH 123/311] Platform dependent efficiency improvements (#1269) --- .../scala/zio/json/internal/SafeNumbers.scala | 2 +- .../scala/zio/json/internal/SafeNumbers.scala | 2 +- .../zio/json/internal/UnsafeNumbers.scala | 16 ++++++------- .../resources/scala-native/multiply_high.c | 7 ++++++ .../scala/zio/json/internal/NativeMath.scala | 13 +++++++++++ .../scala/zio/json/internal/SafeNumbers.scala | 10 ++++---- .../zio/json/internal/UnsafeNumbers.scala | 23 ++++++++----------- 7 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 zio-json/native/src/main/resources/scala-native/multiply_high.c create mode 100644 zio-json/native/src/main/scala/zio/json/internal/NativeMath.scala diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 36e34fc19..ba9721afc 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -165,7 +165,7 @@ object SafeNumbers { val dotOff = s.length + exp + 1 s.append(stripTrailingZeros(dv)) s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') + } else s.append(dv.toInt).append('.').append('0') } s.toString } diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index c2404faf2..eaf47ccf2 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -165,7 +165,7 @@ object SafeNumbers { val dotOff = s.length + exp + 1 s.append(stripTrailingZeros(dv)) s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') + } else s.append(dv.toInt).append('.').append('0') } s.toString } diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index e0745b186..f67b17a9a 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -96,7 +96,7 @@ object UnsafeNumbers { }) { if ( accum < -922337203685477580L || { - accum = (accum << 3) + (accum << 1) + ('0' - current) + accum = accum * 10 + ('0' - current) accum > 0 } ) throw UnsafeNumber @@ -125,7 +125,7 @@ object UnsafeNumbers { }) { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong - else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + else m10 = m10 * 10 + (current - '0') } else { if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) @@ -160,7 +160,7 @@ object UnsafeNumbers { }) { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong - else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + else m10 = m10 * 10 + (current - '0') } else { if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) @@ -177,7 +177,7 @@ object UnsafeNumbers { e10 -= 1 if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong - else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + else m10 = m10 * 10 + (current - '0') } else { if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) @@ -249,7 +249,7 @@ object UnsafeNumbers { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') + m10 = m10 * 10 + (current - '0') digits += 1 } } else { @@ -269,7 +269,7 @@ object UnsafeNumbers { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') + m10 = m10 * 10 + (current - '0') digits += 1 } } else { @@ -388,7 +388,7 @@ object UnsafeNumbers { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') + m10 = m10 * 10 + (current - '0') digits += 1 } } else { @@ -408,7 +408,7 @@ object UnsafeNumbers { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') + m10 = m10 * 10 + (current - '0') digits += 1 } } else { diff --git a/zio-json/native/src/main/resources/scala-native/multiply_high.c b/zio-json/native/src/main/resources/scala-native/multiply_high.c new file mode 100644 index 000000000..6e2411c1c --- /dev/null +++ b/zio-json/native/src/main/resources/scala-native/multiply_high.c @@ -0,0 +1,7 @@ +long zio_json_multiply_high(long x, long y) { + return x * (unsigned __int128) y >> 64; +} + +unsigned long zio_json_unsigned_multiply_high(unsigned long x, unsigned long y) { + return x * (unsigned __int128) y >> 64; +} diff --git a/zio-json/native/src/main/scala/zio/json/internal/NativeMath.scala b/zio-json/native/src/main/scala/zio/json/internal/NativeMath.scala new file mode 100644 index 000000000..d3302a880 --- /dev/null +++ b/zio-json/native/src/main/scala/zio/json/internal/NativeMath.scala @@ -0,0 +1,13 @@ +package zio.json.internal + +import scala.scalanative.unsafe._ + +// FIXME: Replace by an _efficient_ cross-platform version later, see: https://github.com/scala-native/scala-native/issues/2473 +@extern +private[internal] object NativeMath { + @name("zio_json_multiply_high") + def multiplyHigh(x: Long, y: Long): Long = extern + + @name("zio_json_unsigned_multiply_high") + def unsignedMultiplyHigh(x: Long, y: Long): Long = extern +} diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index 8691066ed..2de0f5eec 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -165,7 +165,7 @@ object SafeNumbers { val dotOff = s.length + exp + 1 s.append(stripTrailingZeros(dv)) s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') + } else s.append(dv.toInt).append('.').append('0') } s.toString } @@ -259,12 +259,12 @@ object SafeNumbers { } private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { - val x = Math.multiplyHigh(g0, cp) + (g1 * cp >>> 1) - Math.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63 + val x = NativeMath.multiplyHigh(g0, cp) + (g1 * cp >>> 1) + NativeMath.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63 } private[this] def rop(g: Long, cp: Int): Int = { - val x = ((g & 0xffffffffL) * cp >>> 32) + (g >>> 32) * cp + val x = NativeMath.multiplyHigh(g, cp.toLong << 32) (x >>> 31).toInt | -x.toInt >>> 31 } @@ -272,7 +272,7 @@ object SafeNumbers { var q0 = x.toInt if ( q0 == x || { - q0 = (x / 100000000L).toInt + q0 = (NativeMath.multiplyHigh(x, 6189700196426901375L) >>> 25).toInt // divide a positive long by 100000000 (x - q0 * 100000000L).toInt == 0 } ) return stripTrailingZeros(q0).toLong diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 00e3f1f3e..056e29807 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -96,7 +96,7 @@ object UnsafeNumbers { }) { if ( accum < -922337203685477580L || { - accum = (accum << 3) + (accum << 1) + ('0' - current) + accum = accum * 10 + ('0' - current) accum > 0 } ) throw UnsafeNumber @@ -125,7 +125,7 @@ object UnsafeNumbers { }) { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong - else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + else m10 = m10 * 10 + (current - '0') } else { if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) @@ -160,7 +160,7 @@ object UnsafeNumbers { }) { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong - else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + else m10 = m10 * 10 + (current - '0') } else { if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) @@ -177,7 +177,7 @@ object UnsafeNumbers { e10 -= 1 if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong - else m10 = (m10 << 3) + (m10 << 1) + (current - '0') + else m10 = m10 * 10 + (current - '0') } else { if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) @@ -249,7 +249,7 @@ object UnsafeNumbers { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') + m10 = m10 * 10 + (current - '0') digits += 1 } } else { @@ -269,7 +269,7 @@ object UnsafeNumbers { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') + m10 = m10 * 10 + (current - '0') digits += 1 } } else { @@ -326,7 +326,7 @@ object UnsafeNumbers { else if (e10 >= 39) Float.PositiveInfinity else { var shift = java.lang.Long.numberOfLeadingZeros(m10) - var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var m2 = NativeMath.unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 shift = java.lang.Long.numberOfLeadingZeros(m2) m2 <<= shift @@ -385,7 +385,7 @@ object UnsafeNumbers { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') + m10 = m10 * 10 + (current - '0') digits += 1 } } else { @@ -405,7 +405,7 @@ object UnsafeNumbers { if (m10 < 922337203685477580L) { if (m10 <= 0) m10 = (current - '0').toLong else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') + m10 = m10 * 10 + (current - '0') digits += 1 } } else { @@ -466,7 +466,7 @@ object UnsafeNumbers { else if (e10 >= 310) Double.PositiveInfinity else { var shift = java.lang.Long.numberOfLeadingZeros(m10) - var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var m2 = NativeMath.unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 shift = java.lang.Long.numberOfLeadingZeros(m2) m2 <<= shift @@ -503,9 +503,6 @@ object UnsafeNumbers { if (consume && current != -1) throw UnsafeNumber } - @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = - Math.multiplyHigh(x, y) + x + y // Use implementation that works only when both params are negative - private[this] final val bigIntegers: Array[java.math.BigInteger] = (0L to 9L).map(java.math.BigInteger.valueOf).toArray From 9021f0f2af58139a349c53373317b159b4825ea3 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 31 Jan 2025 15:53:22 +0100 Subject: [PATCH 124/311] Fix a couple of FIXMEs (#1270) --- build.sbt | 3 +-- zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index 2a07d3376..089646a60 100644 --- a/build.sbt +++ b/build.sbt @@ -325,8 +325,7 @@ lazy val zioJsonInteropHttp4s = project "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" ), - testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), - mimaPreviousArtifacts := Set() // FIXME: remove after releasing zio-json-interop-http4s for Scala 3 + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) .dependsOn(zioJsonJVM) .enablePlugins(BuildInfoPlugin) diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index 31309cb31..d7421debb 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -44,9 +44,9 @@ object EncoderSpec extends ZIOSpecDefault { assert(9999999.0f.toJson)(equalTo("9999999.0")) && assert(0.001f.toJson)(equalTo("0.001")) && assert(9.999999e-4f.toJson)(equalTo("9.999999E-4")) && - // FIXME: sbt fmt cannot parse: assert((-3.4028235E38f).toJson)(equalTo("-3.4028235E38")) && // Float.MinValue - assert(1.4e-45f.toJson)(equalTo("1.4E-45")) && // Float.MinPositiveValue - // FIXME: sbt fmt cannot parse: assert(3.4028235E38f.toJson)(equalTo("3.4028235E38")) && // Float.MaxValue + assert(Float.MinValue.toJson)(equalTo("-3.4028235E38")) && + assert(Float.MinPositiveValue.toJson)(equalTo("1.4E-45")) && + assert(Float.MaxValue.toJson)(equalTo("3.4028235E38")) && assert(3.3554448e7f.toJson)(equalTo("3.355445E7")) && assert(8.999999e9f.toJson)(equalTo("9.0E9")) && assert(3.4366718e10f.toJson)(equalTo("3.436672E10")) && From e6af869246ee188c47f67d90dd4ab3374823d457 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 31 Jan 2025 17:14:15 +0100 Subject: [PATCH 125/311] More efficient encoding of doubles with Scala.js (#1271) --- .../scala/zio/json/internal/SafeNumbers.scala | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index ba9721afc..312de00ac 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -127,7 +127,16 @@ object SafeNumbers { val s = vb >> 2 if ( s < 100 || { - dv = s / 10 + var z = s + dv = s + dv = (dv >>> 1) + (dv >>> 2) // Based upon the divu10() code from Hacker's Delight 2nd Edition by Henry Warren + dv += dv >>> 4 + dv += dv >>> 8 + dv += dv >>> 16 + dv += dv >>> 32 + z -= dv & 0xfffffffffffffff8L + dv >>>= 3 + if ((z - (dv << 1)).toInt >= 10) dv += 1L val sp40 = (dv << 5) + (dv << 3) val upin = (vbls - sp40).toInt (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { @@ -293,15 +302,26 @@ object SafeNumbers { q0 * 100000000L == x } ) return stripTrailingZeros(q0).toLong - var y = x - var q1, r1 = 0L + var q1, y, z = x + var r1 = 0 while ({ - q1 = y / 100 - r1 = y - ((q1 << 6) + (q1 << 5) + (q1 << 2)) + q1 = (q1 >>> 1) + (q1 >>> 2) // Based upon the divu10() code from Hacker's Delight 2nd Edition by Henry Warren + q1 += q1 >>> 4 + q1 += q1 >>> 8 + q1 += q1 >>> 16 + q1 += q1 >>> 32 + z -= q1 & 0xfffffffffffffff8L + q1 >>>= 3 + r1 = (z - (q1 << 1)).toInt + if (r1 >= 10) { + q1 += 1L + r1 -= 10 + } r1 == 0 - }) y = q1 - q1 = y / 10 - r1 = y - ((q1 << 3) + (q1 << 1)) + }) { + y = q1 + z = q1 + } if (r1 == 0) return q1 y } From 7d2a0356a9fde7842784685fba69ec8ea3be77d8 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 31 Jan 2025 18:32:59 +0100 Subject: [PATCH 126/311] Simplify `stripTrailingZeros` (#1273) --- .../main/scala/zio/json/internal/SafeNumbers.scala | 1 - .../main/scala/zio/json/internal/SafeNumbers.scala | 13 ++++--------- .../main/scala/zio/json/internal/SafeNumbers.scala | 13 ++++--------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 312de00ac..75b3fe5c4 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -322,7 +322,6 @@ object SafeNumbers { y = q1 z = q1 } - if (r1 == 0) return q1 y } diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index eaf47ccf2..230225737 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -127,7 +127,7 @@ object SafeNumbers { val s = vb >> 2 if ( s < 100 || { - dv = Math.multiplyHigh(s, 1844674407370955168L) + dv = Math.multiplyHigh(s, 1844674407370955168L) // divide a positive long by 10 val sp40 = dv * 40 val upin = (vbls - sp40).toInt (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { @@ -276,16 +276,11 @@ object SafeNumbers { (x - q0 * 100000000L).toInt == 0 } ) return stripTrailingZeros(q0).toLong - var y = x - var q1, r1 = 0L + var y, q1 = x while ({ - q1 = y / 100 - r1 = y - q1 * 100 - r1 == 0 + q1 = Math.multiplyHigh(q1, 1844674407370955168L) // divide a positive long by 10 + q1 * 10 == y }) y = q1 - q1 = y / 10 - r1 = y - q1 * 10 - if (r1 == 0) return q1 y } diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index 2de0f5eec..168369326 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -127,7 +127,7 @@ object SafeNumbers { val s = vb >> 2 if ( s < 100 || { - dv = s / 10 + dv = NativeMath.multiplyHigh(s, 1844674407370955168L) // divide a positive long by 10 val sp40 = dv * 40 val upin = (vbls - sp40).toInt (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { @@ -276,16 +276,11 @@ object SafeNumbers { (x - q0 * 100000000L).toInt == 0 } ) return stripTrailingZeros(q0).toLong - var y = x - var q1, r1 = 0L + var y, q1 = x while ({ - q1 = y / 100 - r1 = y - q1 * 100 - r1 == 0 + q1 = NativeMath.multiplyHigh(q1, 1844674407370955168L) // divide a positive long by 10 + q1 * 10 == y }) y = q1 - q1 = y / 10 - r1 = y - q1 * 10 - if (r1 == 0) return q1 y } From 3c71461fa091b37d7dc08c7511cab16da93c01fd Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 1 Feb 2025 14:48:10 +0100 Subject: [PATCH 127/311] Revert boolean parsing optimization (#1274) --- .../main/scala/zio/json/internal/lexer.scala | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 03491d9c5..b411a052f 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -254,23 +254,25 @@ object Lexer { var c = in.nextNonWhitespace() if (c != '"') error("'\"'", c, trace) c = in.readChar() - if (c == '"') error("expected single character string", trace) - else if (c == '\\') { - (in.readChar(): @switch) match { - case '"' => c = '"' - case '\\' => c = '\\' - case '/' => c = '/' - case 'b' => c = '\b' - case 'f' => c = '\f' - case 'n' => c = '\n' - case 'r' => c = '\r' - case 't' => c = '\t' - case 'u' => c = nextHex4(trace, in) - case _ => error(c, trace) + if ( + c == '"' || { + if (c == '\\') { + (in.readChar(): @switch) match { + case '"' => c = '"' + case '\\' => c = '\\' + case '/' => c = '/' + case 'b' => c = '\b' + case 'f' => c = '\f' + case 'n' => c = '\n' + case 'r' => c = '\r' + case 't' => c = '\t' + case 'u' => c = nextHex4(trace, in) + case _ => error(c, trace) + } + } else if (c < ' ') error("invalid control in string", trace) + in.readChar() != '"' } - } else if (c < ' ') error("invalid control in string", trace) - val c1 = in.readChar() - if (c1 != '"') error("expected single character string", trace) + ) error("expected single character string", trace) c } @@ -292,22 +294,21 @@ object Lexer { accum.toChar } - def boolean(trace: List[JsonError], in: OneCharReader): Boolean = { - val c1 = in.nextNonWhitespace() - val c2 = in.readChar() - val c3 = in.readChar() - val c4 = in.readChar() - (c1: @switch) match { + def boolean(trace: List[JsonError], in: OneCharReader): Boolean = + (in.nextNonWhitespace(): @switch) match { case 't' => - if (c2 != 'r' || c3 != 'u' || c4 != 'e') error("expected 'true'", trace) + if (in.readChar() != 'r' || in.readChar() != 'u' || in.readChar() != 'e') { + error("expected 'true'", trace) + } true case 'f' => - if (in.readChar() != 'e' || c2 != 'a' || c3 != 'l' || c4 != 's') error("expected 'false'", trace) + if (in.readChar() != 'a' || in.readChar() != 'l' || in.readChar() != 's' || in.readChar() != 'e') { + error("expected 'false'", trace) + } false case c => error("'true' or 'false'", c, trace) } - } def byte(trace: List[JsonError], in: RetractReader): Byte = try { From 95e7f2e5fb8edbd17cb1154eb15fbffa4e0f59a7 Mon Sep 17 00:00:00 2001 From: Adam Hearn <22334119+hearnadam@users.noreply.github.com> Date: Sat, 1 Feb 2025 05:49:33 -0800 Subject: [PATCH 128/311] Use cached empty AST instances in `apply` (#1272) --- .../src/main/scala/zio/json/ast/ast.scala | 24 +++++++++++++++---- .../test/scala/zio/json/ast/JsonSpec.scala | 11 +++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index 535003db1..fca4c9eeb 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -376,9 +376,15 @@ object Json { override def mapObjectEntries(f: ((String, Json)) => (String, Json)): Json.Obj = Json.Obj(fields.map(f)) } object Obj { - val empty: Obj = Obj(Chunk.empty) + val empty: Obj = new Obj(Chunk.empty) - def apply(fields: (String, Json)*): Obj = Obj(Chunk(fields: _*)) + def apply(chunk: Chunk[(String, Json)]): Obj = + if (chunk.isEmpty) empty + else new Obj(chunk) + + def apply(fields: (String, Json)*): Obj = + if (fields.isEmpty) Obj.empty + else new Obj(Chunk(fields: _*)) private lazy val objd = JsonDecoder.keyValueChunk[String, Json] implicit val decoder: JsonDecoder[Obj] = new JsonDecoder[Obj] { @@ -423,9 +429,15 @@ object Json { override def mapArrayValues(f: Json => Json): Json.Arr = Json.Arr(elements.map(f)) } object Arr { - val empty: Arr = Arr(Chunk.empty) + val empty: Arr = new Arr(Chunk.empty) + + def apply(chunk: Chunk[Json]): Arr = + if (chunk.isEmpty) empty + else new Arr(chunk) - def apply(elements: Json*): Arr = Arr(Chunk(elements: _*)) + def apply(elements: Json*): Arr = + if (elements.isEmpty) empty + else new Arr(Chunk(elements: _*)) private lazy val arrd = JsonDecoder.chunk[Json] implicit val decoder: JsonDecoder[Arr] = new JsonDecoder[Arr] { @@ -595,5 +607,7 @@ object Json { implicit val codec: JsonCodec[Json] = JsonCodec(encoder, decoder) - def apply(fields: (String, Json)*): Json = Json.Obj(Chunk(fields: _*)) + def apply(fields: (String, Json)*): Json = + if (fields.isEmpty) Obj.empty + else new Obj(Chunk(fields: _*)) } diff --git a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala index dca36e625..fcfc04e3c 100644 --- a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala @@ -9,6 +9,17 @@ object JsonSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = suite("Json")( + suite("apply")( + test("()") { + assertTrue(Json.Obj.empty eq Json()) && + assertTrue(Json.Obj.empty eq Json.Obj()) && + assertTrue(Json.Arr.empty eq Json.Arr()) + }, + test("(Chunk.empty)") { + assertTrue(Json.Obj.empty eq Json.Obj(Chunk.empty)) && + assertTrue(Json.Arr.empty eq Json.Arr(Chunk.empty)) + } + ), suite("delete")( suite("scalar")( test("success") { From c40b7a66036749b706a6c058afae4f46d366a121 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 2 Feb 2025 08:47:49 +0100 Subject: [PATCH 129/311] More efficient decoding of `BigInt` and `java.math.BigInteger` values (#1275) --- .../scala/zio/json/internal/SafeNumbers.scala | 10 +--- .../zio/json/internal/UnsafeNumbers.scala | 38 ++++++++----- .../scala/zio/json/internal/SafeNumbers.scala | 10 +--- .../zio/json/internal/UnsafeNumbers.scala | 54 ++++++++++--------- .../scala/zio/json/internal/SafeNumbers.scala | 10 +--- .../zio/json/internal/UnsafeNumbers.scala | 38 ++++++++----- .../src/main/scala/zio/json/JsonDecoder.scala | 2 +- .../main/scala/zio/json/internal/lexer.scala | 27 ++-------- 8 files changed, 89 insertions(+), 100 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 75b3fe5c4..0c9718229 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -56,10 +56,7 @@ object SafeNumbers { try LongSome(UnsafeNumbers.long(num)) catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - def bigInteger( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigInteger] = + def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } @@ -71,10 +68,7 @@ object SafeNumbers { try DoubleSome(UnsafeNumbers.double(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - def bigDecimal( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigDecimal] = + def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 24e25633a..4c8e7fd29 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -117,28 +117,38 @@ object UnsafeNumbers { val negate = current == '-' if (negate) current = in.readChar().toInt if (current < '0' || current > '9') throw UnsafeNumber - var bigM10: java.math.BigInteger = null - var m10 = (current - '0').toLong + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else m10 = (m10 << 3) + (m10 << 1) + (current - '0') - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 } } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { - if (negate) m10 = -m10 - return java.math.BigInteger.valueOf(m10) + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return java.math.BigInteger.valueOf(loM10) } - if (negate) bigM10 = bigM10.negate - bigM10 + if (loDigits != 0) { + if (negate) loM10 = -loM10 + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + hiM10.unscaledValue } def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index 230225737..4a6489a39 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -56,10 +56,7 @@ object SafeNumbers { try LongSome(UnsafeNumbers.long(num)) catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - def bigInteger( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigInteger] = + def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } @@ -71,10 +68,7 @@ object SafeNumbers { try DoubleSome(UnsafeNumbers.double(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - def bigDecimal( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigDecimal] = + def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index f67b17a9a..4d9b34375 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -117,28 +117,38 @@ object UnsafeNumbers { val negate = current == '-' if (negate) current = in.readChar().toInt if (current < '0' || current > '9') throw UnsafeNumber - var bigM10: java.math.BigInteger = null - var m10 = (current - '0').toLong + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else m10 = m10 * 10 + (current - '0') - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 } } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { - if (negate) m10 = -m10 - return java.math.BigInteger.valueOf(m10) + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return java.math.BigInteger.valueOf(loM10) } - if (negate) bigM10 = bigM10.negate - bigM10 + if (loDigits != 0) { + if (negate) loM10 = -loM10 + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + hiM10.unscaledValue } def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = @@ -326,11 +336,8 @@ object UnsafeNumbers { else if (e10 >= 39) Float.PositiveInfinity else { var shift = java.lang.Long.numberOfLeadingZeros(m10) - var m2 = unsignedMultiplyHigh( - pow10Mantissas(e10 + 343), - m10 << shift - ) // FIXME: Use Math.unsignedMultiplyHigh after dropping of JDK 17 support - var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 + var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 shift = java.lang.Long.numberOfLeadingZeros(m2) m2 <<= shift e2 -= shift @@ -469,11 +476,8 @@ object UnsafeNumbers { else if (e10 >= 310) Double.PositiveInfinity else { var shift = java.lang.Long.numberOfLeadingZeros(m10) - var m2 = unsignedMultiplyHigh( - pow10Mantissas(e10 + 343), - m10 << shift - ) // FIXME: Use Math.unsignedMultiplyHigh after dropping of JDK 17 support - var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 + var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 shift = java.lang.Long.numberOfLeadingZeros(m2) m2 <<= shift e2 -= shift @@ -510,7 +514,7 @@ object UnsafeNumbers { } @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = - Math.multiplyHigh(x, y) + x + y // Use implementation that works only when both params are negative + Math.multiplyHigh(x, y) + x + y // FIXME: Use Math.unsignedMultiplyHigh after dropping of JDK 17 support private[this] final val bigIntegers: Array[java.math.BigInteger] = (0L to 9L).map(java.math.BigInteger.valueOf).toArray diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index 168369326..fcecf03a4 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -56,10 +56,7 @@ object SafeNumbers { try LongSome(UnsafeNumbers.long(num)) catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - def bigInteger( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigInteger] = + def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } @@ -71,10 +68,7 @@ object SafeNumbers { try DoubleSome(UnsafeNumbers.double(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - def bigDecimal( - num: String, - max_bits: Int = 128 - ): Option[java.math.BigDecimal] = + def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 056e29807..74250d593 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -117,28 +117,38 @@ object UnsafeNumbers { val negate = current == '-' if (negate) current = in.readChar().toInt if (current < '0' || current > '9') throw UnsafeNumber - var bigM10: java.math.BigInteger = null - var m10 = (current - '0').toLong + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else m10 = m10 * 10 + (current - '0') - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 } } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { - if (negate) m10 = -m10 - return java.math.BigInteger.valueOf(m10) + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return java.math.BigInteger.valueOf(loM10) } - if (negate) bigM10 = bigM10.negate - bigM10 + if (loDigits != 0) { + if (negate) loM10 = -loM10 + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + hiM10.unscaledValue } def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 43272c230..f03f30bf4 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -325,7 +325,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with implicit val int: JsonDecoder[Int] = number(Lexer.int, _.intValueExact()) implicit val long: JsonDecoder[Long] = number(Lexer.long, _.longValueExact()) implicit val bigInteger: JsonDecoder[java.math.BigInteger] = number(Lexer.bigInteger, _.toBigIntegerExact) - implicit val scalaBigInt: JsonDecoder[BigInt] = bigInteger.map(x => x) + implicit val scalaBigInt: JsonDecoder[BigInt] = number(Lexer.bigInteger, _.toBigIntegerExact) implicit val float: JsonDecoder[Float] = number(Lexer.float, _.floatValue()) implicit val double: JsonDecoder[Double] = number(Lexer.double, _.doubleValue()) implicit val bigDecimal: JsonDecoder[java.math.BigDecimal] = number(Lexer.bigDecimal, identity) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index b411a052f..6f93bfc89 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -78,21 +78,13 @@ object Lexer { // messages) by only checking for what we expect to see (Jon Pretty's idea). // // returns the index of the matched field, or -1 - def field( - trace: List[JsonError], - in: OneCharReader, - matrix: StringMatrix - ): Int = { + def field(trace: List[JsonError], in: OneCharReader, matrix: StringMatrix): Int = { val f = enumeration(trace, in, matrix) char(trace, in, ':') f } - def enumeration( - trace: List[JsonError], - in: OneCharReader, - matrix: StringMatrix - ): Int = { + def enumeration(trace: List[JsonError], in: OneCharReader, matrix: StringMatrix): Int = { var c = in.nextNonWhitespace() if (c != '"') error("'\"'", c, trace) var bs = matrix.initial @@ -181,10 +173,7 @@ object Lexer { } // useful for embedded documents, e.g. CSV contained inside JSON - def streamingString( - trace: List[JsonError], - in: OneCharReader - ): java.io.Reader = { + def streamingString(trace: List[JsonError], in: OneCharReader): java.io.Reader = { char(trace, in, '"') new OneCharReader { def close(): Unit = in.close() @@ -346,10 +335,7 @@ object Lexer { case UnsafeNumbers.UnsafeNumber => error("expected a Long", trace) } - def bigInteger( - trace: List[JsonError], - in: RetractReader - ): java.math.BigInteger = + def bigInteger(trace: List[JsonError], in: RetractReader): java.math.BigInteger = try { val i = UnsafeNumbers.bigInteger_(in, false, NumberMaxBits) in.retract() @@ -376,10 +362,7 @@ object Lexer { case UnsafeNumbers.UnsafeNumber => error("expected a Double", trace) } - def bigDecimal( - trace: List[JsonError], - in: RetractReader - ): java.math.BigDecimal = + def bigDecimal(trace: List[JsonError], in: RetractReader): java.math.BigDecimal = try { val i = UnsafeNumbers.bigDecimal_(in, false, NumberMaxBits) in.retract() From 320961211e218c4615481fdce7ebed2ae45b852a Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 2 Feb 2025 12:13:40 +0100 Subject: [PATCH 130/311] More efficient decoding of `BigDecimal` values (#1276) --- .../zio/json/internal/UnsafeNumbers.scala | 63 +++++--- .../zio/json/internal/UnsafeNumbers.scala | 63 +++++--- .../zio/json/internal/UnsafeNumbers.scala | 63 +++++--- .../src/main/scala/zio/json/JsonDecoder.scala | 142 +++++++----------- .../main/scala/zio/json/internal/lexer.scala | 47 ++---- 5 files changed, 193 insertions(+), 185 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 4c8e7fd29..e4c05fcc5 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -160,21 +160,28 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - var bigM10: java.math.BigInteger = null - var m10 = -1L + var loM10 = 0L + var loDigits = 0 + var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { - m10 = (current - '0').toLong + loM10 = (current - '0').toLong + loDigits += 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else m10 = (m10 << 3) + (m10 << 1) + (current - '0') - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 } } } @@ -184,18 +191,23 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 e10 -= 1 - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else m10 = (m10 << 3) + (m10 << 1) + (current - '0') - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 } } } - if (m10 < 0) throw UnsafeNumber + if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -218,12 +230,17 @@ object UnsafeNumbers { else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { - if (negate) m10 = -m10 - return java.math.BigDecimal.valueOf(m10, -e10) + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return java.math.BigDecimal.valueOf(loM10, -e10) } - if (negate) bigM10 = bigM10.negate - new java.math.BigDecimal(bigM10, -e10) + hiM10 = hiM10.scaleByPowerOfTen(loDigits + e10) + if (loDigits != 0) { + if (negate) loM10 = -loM10 + hiM10 = hiM10.add(java.math.BigDecimal.valueOf(loM10, -e10)) + } + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + hiM10 } def float(num: String, max_bits: Int): Float = diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 4d9b34375..a90ce7008 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -160,21 +160,28 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - var bigM10: java.math.BigInteger = null - var m10 = -1L + var loM10 = 0L + var loDigits = 0 + var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { - m10 = (current - '0').toLong + loM10 = (current - '0').toLong + loDigits += 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else m10 = m10 * 10 + (current - '0') - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 } } } @@ -184,18 +191,23 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 e10 -= 1 - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else m10 = m10 * 10 + (current - '0') - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 } } } - if (m10 < 0) throw UnsafeNumber + if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -218,12 +230,17 @@ object UnsafeNumbers { else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { - if (negate) m10 = -m10 - return java.math.BigDecimal.valueOf(m10, -e10) + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return java.math.BigDecimal.valueOf(loM10, -e10) } - if (negate) bigM10 = bigM10.negate - new java.math.BigDecimal(bigM10, -e10) + hiM10 = hiM10.scaleByPowerOfTen(loDigits + e10) + if (loDigits != 0) { + if (negate) loM10 = -loM10 + hiM10 = hiM10.add(java.math.BigDecimal.valueOf(loM10, -e10)) + } + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + hiM10 } def float(num: String, max_bits: Int): Float = diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 74250d593..f831cd724 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -160,21 +160,28 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - var bigM10: java.math.BigInteger = null - var m10 = -1L + var loM10 = 0L + var loDigits = 0 + var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { - m10 = (current - '0').toLong + loM10 = (current - '0').toLong + loDigits += 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else m10 = m10 * 10 + (current - '0') - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 } } } @@ -184,18 +191,23 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 e10 -= 1 - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else m10 = m10 * 10 + (current - '0') - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + if (loM10 >= 100000000000000000L) { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = 0 + loDigits = 0 } } } - if (m10 < 0) throw UnsafeNumber + if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -218,12 +230,17 @@ object UnsafeNumbers { else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { - if (negate) m10 = -m10 - return java.math.BigDecimal.valueOf(m10, -e10) + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return java.math.BigDecimal.valueOf(loM10, -e10) } - if (negate) bigM10 = bigM10.negate - new java.math.BigDecimal(bigM10, -e10) + hiM10 = hiM10.scaleByPowerOfTen(loDigits + e10) + if (loDigits != 0) { + if (negate) loM10 = -loM10 + hiM10 = hiM10.add(java.math.BigDecimal.valueOf(loM10, -e10)) + } + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + hiM10 } def float(num: String, max_bits: Int): Float = diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index f03f30bf4..b36f5b9bc 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -22,7 +22,6 @@ import zio.json.uuid.UUIDParser import zio.{ Chunk, NonEmptyChunk } import java.util.UUID -import scala.annotation._ import scala.collection.immutable.{ LinearSeq, ListSet, TreeSet } import scala.collection.{ immutable, mutable } import scala.util.control.NoStackTrace @@ -71,10 +70,10 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { final def bothWith[B, C](that: => JsonDecoder[B])(f: (A, B) => C): JsonDecoder[C] = new JsonDecoder[C] { override def unsafeDecode(trace: List[JsonError], in: RetractReader): C = { - val in2 = new WithRecordingReader(in, 64) - val a = self.unsafeDecode(trace, in2) - in2.rewind() - val b = that.unsafeDecode(trace, in2) + val rr = RecordingReader(in) + val a = self.unsafeDecode(trace, rr) + rr.rewind() + val b = that.unsafeDecode(trace, rr) f(a, b) } } @@ -116,24 +115,19 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { new JsonDecoder[A1] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A1 = { - val in2 = new zio.json.internal.WithRecordingReader(in, 64) - - try self.unsafeDecode(trace, in2) + val rr = RecordingReader(in) + try self.unsafeDecode(trace, rr) catch { - case JsonDecoder.UnsafeJson(_) => - in2.rewind() - that.unsafeDecode(trace, in2) - - case _: UnexpectedEnd => - in2.rewind() - that.unsafeDecode(trace, in2) + case _: JsonDecoder.UnsafeJson | _: UnexpectedEnd => + rr.rewind() + that.unsafeDecode(trace, rr) } } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A1 = try self.unsafeFromJsonAST(trace, json) catch { - case JsonDecoder.UnsafeJson(_) | _: UnexpectedEnd => that.unsafeFromJsonAST(trace, json) + case _: JsonDecoder.UnsafeJson | _: UnexpectedEnd => that.unsafeFromJsonAST(trace, json) } override def unsafeDecodeMissing(trace: List[JsonError]): A1 = @@ -141,7 +135,6 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { catch { case _: Throwable => that.unsafeDecodeMissing(trace) } - } /** @@ -149,7 +142,7 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { * codec fails, the specified codec will be tried instead. */ final def orElseEither[B](that: => JsonDecoder[B]): JsonDecoder[Either[A, B]] = - self.map(Left(_)).orElse(that.map(Right(_))) + self.map(new Left(_)).orElse(that.map(new Right(_))) /** * Returns a new codec whose decoded values will be mapped by the specified function. @@ -166,7 +159,6 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { override def unsafeDecodeMissing(trace: List[JsonError]): B = f(self.unsafeDecodeMissing(trace)) - } /** @@ -193,7 +185,6 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { case Right(b) => b case Left(err) => Lexer.error(err, trace) } - } /** @@ -243,7 +234,6 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { case _: UnexpectedEnd => Left("Unexpected end of input") case _: StackOverflowError => Left("Unexpected structure") } - } object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with JsonDecoderVersionSpecific { @@ -329,7 +319,8 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with implicit val float: JsonDecoder[Float] = number(Lexer.float, _.floatValue()) implicit val double: JsonDecoder[Double] = number(Lexer.double, _.doubleValue()) implicit val bigDecimal: JsonDecoder[java.math.BigDecimal] = number(Lexer.bigDecimal, identity) - implicit val scalaBigDecimal: JsonDecoder[BigDecimal] = bigDecimal.map(x => x) + implicit val scalaBigDecimal: JsonDecoder[BigDecimal] = + number(Lexer.bigDecimal, x => new BigDecimal(x, BigDecimal.defaultMathContext)) // numbers decode from numbers or strings for maximum compatibility private[this] def number[A]( @@ -339,14 +330,14 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = - (in.nextNonWhitespace(): @switch) match { - case '"' => - val i = f(trace, in) - Lexer.charOnly(trace, in, '"') - i - case _ => - in.retract() - f(trace, in) + if (in.nextNonWhitespace() == '"') { + val a = f(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } else { + in.retract() + f(trace, in) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = @@ -354,13 +345,10 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case Json.Num(value) => try fromBigDecimal(value) catch { - case exception: ArithmeticException => Lexer.error(exception.getMessage, trace) + case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) } - case Json.Str(value) => - val reader = new FastStringReader(value) - try f(List.empty, reader) - finally reader.close() - case _ => Lexer.error("Not a number or a string", trace) + case Json.Str(value) => f(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) } } @@ -427,8 +415,8 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with }) () if (left == null && right == null) Lexer.error("missing fields", trace) if (left != null && right != null) Lexer.error("ambiguous either, zip present", trace) - if (left != null) Left(left.asInstanceOf[A]) - else Right(right.asInstanceOf[B]) + if (left != null) new Left(left.asInstanceOf[A]) + else new Right(right.asInstanceOf[B]) } } @@ -437,14 +425,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with in: RetractReader, builder: mutable.Builder[A, T[A]] )(implicit A: JsonDecoder[A]): T[A] = { - Lexer.char(trace, in, '[') + val c = in.nextNonWhitespace() + if (c != '[') Lexer.error("'['", c, trace) var i: Int = 0 if (Lexer.firstArrayElement(in)) while ({ - { - val trace_ = JsonError.ArrayAccess(i) :: trace - builder += A.unsafeDecode(trace_, in) - i += 1 - }; Lexer.nextArrayElement(trace, in) + builder += A.unsafeDecode(JsonError.ArrayAccess(i) :: trace, in) + i += 1 + Lexer.nextArrayElement(trace, in) }) () builder.result() } @@ -454,16 +441,16 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with in: RetractReader, builder: mutable.Builder[(K, V), T[K, V]] )(implicit K: JsonFieldDecoder[K], V: JsonDecoder[V]): T[K, V] = { - Lexer.char(trace, in, '{') + val c = in.nextNonWhitespace() + if (c != '{') Lexer.error("'{'", c, trace) if (Lexer.firstField(trace, in)) while ({ - { - val field = Lexer.string(trace, in).toString - val trace_ = JsonError.ObjectAccess(field) :: trace - Lexer.char(trace_, in, ':') - val value = V.unsafeDecode(trace_, in) - builder += ((K.unsafeDecodeField(trace_, field), value)) - }; Lexer.nextField(trace, in) + val field = Lexer.string(trace, in).toString + val trace_ = JsonError.ObjectAccess(field) :: trace + Lexer.char(trace_, in, ':') + val value = V.unsafeDecode(trace_, in) + builder += ((K.unsafeDecodeField(trace_, field), value)) + Lexer.nextField(trace, in) }) () builder.result() } @@ -506,8 +493,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def seq[A: JsonDecoder]: JsonDecoder[Seq[A]] = new CollectionJsonDecoder[Seq[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Seq[A] = - Seq.empty + override def unsafeDecodeMissing(trace: List[JsonError]): Seq[A] = Seq.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Seq[A] = builder(trace, in, immutable.Seq.newBuilder[A]) @@ -549,8 +535,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def linearSeq[A: JsonDecoder]: JsonDecoder[immutable.LinearSeq[A]] = new CollectionJsonDecoder[immutable.LinearSeq[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.LinearSeq[A] = - immutable.LinearSeq.empty + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.LinearSeq[A] = immutable.LinearSeq.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): LinearSeq[A] = builder(trace, in, immutable.LinearSeq.newBuilder[A]) @@ -558,8 +543,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def listSet[A: JsonDecoder]: JsonDecoder[immutable.ListSet[A]] = new CollectionJsonDecoder[immutable.ListSet[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListSet[A] = - immutable.ListSet.empty + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListSet[A] = immutable.ListSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): ListSet[A] = builder(trace, in, immutable.ListSet.newBuilder[A]) @@ -567,8 +551,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def treeSet[A: JsonDecoder: Ordering]: JsonDecoder[immutable.TreeSet[A]] = new CollectionJsonDecoder[immutable.TreeSet[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.TreeSet[A] = - immutable.TreeSet.empty + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.TreeSet[A] = immutable.TreeSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): TreeSet[A] = builder(trace, in, immutable.TreeSet.newBuilder[A]) @@ -600,8 +583,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def hashSet[A: JsonDecoder]: JsonDecoder[immutable.HashSet[A]] = new CollectionJsonDecoder[immutable.HashSet[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashSet[A] = - immutable.HashSet.empty + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashSet[A] = immutable.HashSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashSet[A] = builder(trace, in, immutable.HashSet.newBuilder[A]) @@ -617,8 +599,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def hashMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.HashMap[K, V]] = new CollectionJsonDecoder[immutable.HashMap[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashMap[K, V] = - immutable.HashMap.empty + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.HashMap[K, V] = immutable.HashMap.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.HashMap[K, V] = keyValueBuilder(trace, in, immutable.HashMap.newBuilder[K, V]) @@ -626,8 +607,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def mutableMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[mutable.Map[K, V]] = new CollectionJsonDecoder[mutable.Map[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): mutable.Map[K, V] = - mutable.Map.empty + override def unsafeDecodeMissing(trace: List[JsonError]): mutable.Map[K, V] = mutable.Map.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): mutable.Map[K, V] = keyValueBuilder(trace, in, mutable.Map.newBuilder[K, V]) @@ -635,8 +615,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def sortedSet[A: Ordering: JsonDecoder]: JsonDecoder[immutable.SortedSet[A]] = new CollectionJsonDecoder[immutable.SortedSet[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.SortedSet[A] = - immutable.SortedSet.empty + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.SortedSet[A] = immutable.SortedSet.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.SortedSet[A] = builder(trace, in, immutable.SortedSet.newBuilder[A]) @@ -644,8 +623,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def sortedMap[K: JsonFieldDecoder: Ordering, V: JsonDecoder]: JsonDecoder[collection.SortedMap[K, V]] = new CollectionJsonDecoder[collection.SortedMap[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): collection.SortedMap[K, V] = - collection.SortedMap.empty + override def unsafeDecodeMissing(trace: List[JsonError]): collection.SortedMap[K, V] = collection.SortedMap.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): collection.SortedMap[K, V] = keyValueBuilder(trace, in, collection.SortedMap.newBuilder[K, V]) @@ -653,8 +631,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { implicit def listMap[K: JsonFieldDecoder, V: JsonDecoder]: JsonDecoder[immutable.ListMap[K, V]] = new CollectionJsonDecoder[immutable.ListMap[K, V]] { - override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListMap[K, V] = - immutable.ListMap.empty + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ListMap[K, V] = immutable.ListMap.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.ListMap[K, V] = keyValueBuilder(trace, in, immutable.ListMap.newBuilder[K, V]) @@ -698,7 +675,6 @@ private[json] trait DecoderLowPriority2 extends DecoderLowPriority3 { zio.ChunkBuilder.make[(K, A)]() ) } - } private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { @@ -734,8 +710,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } // Commonized handling for decoding from string to java.time Class - @inline - private[this] def parseJavaTime(trace: List[JsonError], s: String): A = + @inline private[this] def parseJavaTime(trace: List[JsonError], s: String): A = try f(s) catch { case ex: DateTimeException => @@ -747,12 +722,12 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { // Commonized handling for decoding from string to java.time Class private[json] def parseJavaTime[A](f: String => A, s: String): Either[String, A] = - try Right(f(s)) + try new Right(f(s)) catch { case ex: DateTimeException => - Left(s"${strip(s)} is not a valid ISO-8601 format, ${ex.getMessage}") + new Left(s"${strip(s)} is not a valid ISO-8601 format, ${ex.getMessage}") case _: IllegalArgumentException => - Left(s"${strip(s)} is not a valid ISO-8601 format") + new Left(s"${strip(s)} is not a valid ISO-8601 format") } implicit val uuid: JsonDecoder[UUID] = new JsonDecoder[UUID] { @@ -765,8 +740,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { case _ => Lexer.error("expected string", trace) } - @inline - private[this] def parseUUID(trace: List[JsonError], s: String): UUID = + @inline private[this] def parseUUID(trace: List[JsonError], s: String): UUID = try UUIDParser.unsafeParse(s) catch { case _: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${strip(s)}", trace) @@ -783,20 +757,18 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { case _ => Lexer.error("expected string", trace) } - @inline - private[this] def parseCurrency(trace: List[JsonError], s: String): java.util.Currency = + @inline private[this] def parseCurrency(trace: List[JsonError], s: String): java.util.Currency = try java.util.Currency.getInstance(s) catch { case _: IllegalArgumentException => Lexer.error(s"Invalid Currency: ${strip(s)}", trace) } } - private[json] def strip(s: String, len: Int = 50): String = + @noinline private[json] def strip(s: String, len: Int = 50): String = if (s.length <= len) s else s.substring(0, len) + "..." } private[json] trait DecoderLowPriority4 extends DecoderLowPriorityVersionSpecific { - @inline - implicit def fromCodec[A](implicit codec: JsonCodec[A]): JsonDecoder[A] = codec.decoder + @inline implicit def fromCodec[A](implicit codec: JsonCodec[A]): JsonDecoder[A] = codec.decoder } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 6f93bfc89..5a27e318f 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -28,16 +28,13 @@ object Lexer { val NumberMaxBits: Int = 256 - @noinline - def error(msg: String, trace: List[JsonError]): Nothing = + @noinline def error(msg: String, trace: List[JsonError]): Nothing = throw UnsafeJson(JsonError.Message(msg) :: trace) - @noinline - private[json] def error(expected: String, got: Char, trace: List[JsonError]): Nothing = + @noinline private[json] def error(expected: String, got: Char, trace: List[JsonError]): Nothing = error(s"expected $expected got '$got'", trace) - @noinline - private[json] def error(c: Char, trace: List[JsonError]): Nothing = + @noinline private[json] def error(c: Char, trace: List[JsonError]): Nothing = error(s"invalid '\\$c' in string", trace) // True if we got a string (implies a retraction), False for } @@ -60,11 +57,9 @@ object Lexer { // True if we got anything besides a ], False for ] def firstArrayElement(in: RetractReader): Boolean = - (in.nextNonWhitespace(): @switch) match { - case ']' => false - case _ => - in.retract() - true + in.nextNonWhitespace() != ']' && { + in.retract() + true } def nextArrayElement(trace: List[JsonError], in: OneCharReader): Boolean = @@ -74,13 +69,10 @@ object Lexer { case c => error("',' or ']'", c, trace) } - // avoids allocating lots of strings (they are often the bulk of incoming - // messages) by only checking for what we expect to see (Jon Pretty's idea). - // - // returns the index of the matched field, or -1 def field(trace: List[JsonError], in: OneCharReader, matrix: StringMatrix): Int = { val f = enumeration(trace, in, matrix) - char(trace, in, ':') + val c = in.nextNonWhitespace() + if (c != ':') error("':'", c, trace) f } @@ -135,23 +127,20 @@ object Lexer { def skipString(trace: List[JsonError], in: OneCharReader): Unit = skipString(in, evenBackSlashes = true) - @tailrec - private def skipFixedChars(in: OneCharReader, n: Int): Unit = + @tailrec private def skipFixedChars(in: OneCharReader, n: Int): Unit = if (n > 0) { in.readChar() skipFixedChars(in, n - 1) } - @tailrec - private def skipString(in: OneCharReader, evenBackSlashes: Boolean): Unit = { + @tailrec private def skipString(in: OneCharReader, evenBackSlashes: Boolean): Unit = { val ch = in.readChar() if (evenBackSlashes) { if (ch != '"') skipString(in, ch != '\\') } else skipString(in, evenBackSlashes = true) } - @tailrec - private def skipObject(in: OneCharReader, level: Int): Unit = { + @tailrec private def skipObject(in: OneCharReader, level: Int): Unit = { val ch = in.readChar() if (ch == '"') { skipString(in, evenBackSlashes = true) @@ -161,8 +150,7 @@ object Lexer { else if (level != 0) skipObject(in, level - 1) } - @tailrec - private def skipArray(in: OneCharReader, level: Int): Unit = { + @tailrec private def skipArray(in: OneCharReader, level: Int): Unit = { val b = in.readChar() if (b == '"') { skipString(in, evenBackSlashes = true) @@ -180,8 +168,7 @@ object Lexer { private[this] var escaped = false - @tailrec - override def read(): Int = { + @tailrec override def read(): Int = { val c = in.readChar() if (escaped) { escaped = false @@ -266,8 +253,7 @@ object Lexer { } // consumes 4 hex characters after current - @noinline - def nextHex4(trace: List[JsonError], in: OneCharReader): Char = { + @noinline def nextHex4(trace: List[JsonError], in: OneCharReader): Char = { var i, accum = 0 while (i < 4) { val c = in.readChar() @@ -389,9 +375,8 @@ object Lexer { // non-positional for performance @inline private[this] def isNumber(c: Char): Boolean = (c: @switch) match { - case '+' | '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' | 'e' | 'E' => - true - case _ => false + case '+' | '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' | 'e' | 'E' => true + case _ => false } def readChars( From f5337c0917bdb87af4bbc1406f4633b0da4a74c2 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 2 Feb 2025 14:38:30 +0100 Subject: [PATCH 131/311] More efficient decoding of floats and doubles with long mantissas (#1277) --- .../zio/json/internal/UnsafeNumbers.scala | 165 ++++++++---------- .../zio/json/internal/UnsafeNumbers.scala | 165 ++++++++---------- .../zio/json/internal/UnsafeNumbers.scala | 165 ++++++++---------- 3 files changed, 225 insertions(+), 270 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index e4c05fcc5..b358ffb6b 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -173,13 +173,7 @@ object UnsafeNumbers { loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') loDigits += 1 if (loM10 >= 100000000000000000L) { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) loM10 = 0 loDigits = 0 } @@ -195,13 +189,7 @@ object UnsafeNumbers { loDigits += 1 e10 -= 1 if (loM10 >= 100000000000000000L) { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) loM10 = 0 loDigits = 0 } @@ -234,11 +222,27 @@ object UnsafeNumbers { if (negate) loM10 = -loM10 return java.math.BigDecimal.valueOf(loM10, -e10) } - hiM10 = hiM10.scaleByPowerOfTen(loDigits + e10) - if (loDigits != 0) { - if (negate) loM10 = -loM10 - hiM10 = hiM10.add(java.math.BigDecimal.valueOf(loM10, -e10)) - } + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) + } + + @noinline private[this] def toBigDecimal( + hi: java.math.BigDecimal, + lo: Long, + loDigits: Int, + e10: Int, + max_bits: Int, + negate: Boolean + ): java.math.BigDecimal = { + var loM10 = lo + if (negate) loM10 = -loM10 + val bd = + if (loDigits != 0) java.math.BigDecimal.valueOf(loM10, -e10) + else java.math.BigDecimal.ZERO + if (hi eq null) return bd + var hiM10 = hi + val scale = loDigits + e10 + if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) + hiM10 = hiM10.add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber hiM10 } @@ -264,25 +268,22 @@ object UnsafeNumbers { readAll(in, "nfinity", consume) return if (negate) Float.NegativeInfinity else Float.PositiveInfinity } - var digits = 1 // calculate digits for m10 only - var m10 = -1L - var bigM10: java.math.BigInteger = null + var loM10 = 0L + var loDigits = 0 + var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { - m10 = (current - '0').toLong + loM10 = (current - '0').toLong + loDigits += 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } @@ -292,21 +293,17 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 e10 -= 1 - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } - if (m10 < 0) throw UnsafeNumber + if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -329,21 +326,20 @@ object UnsafeNumbers { else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { + if (hiM10 eq null) { var x: Float = - if (e10 == 0) m10.toFloat + if (e10 == 0) loM10.toFloat else { - if (m10 < 4294967296L && e10 >= digits - 23 && e10 <= 19 - digits) { + if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { val pow10 = pow10Doubles - (if (e10 < 0) m10 / pow10(-e10) - else m10 * pow10(e10)).toFloat - } else toFloat(m10, e10) + (if (e10 < 0) loM10 / pow10(-e10) + else loM10 * pow10(e10)).toFloat + } else toFloat(loM10, e10) } if (negate) x = -x return x } - if (negate) bigM10 = bigM10.negate - new java.math.BigDecimal(bigM10, -e10).floatValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue() } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -400,25 +396,22 @@ object UnsafeNumbers { readAll(in, "nfinity", consume) return if (negate) Double.NegativeInfinity else Double.PositiveInfinity } - var digits = 1 // calculate digits for m10 only - var m10 = -1L - var bigM10: java.math.BigInteger = null + var loM10 = 0L + var loDigits = 0 + var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { - m10 = (current - '0').toLong + loM10 = (current - '0').toLong + loDigits += 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } @@ -428,21 +421,17 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 e10 -= 1 - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = (m10 << 3) + (m10 << 1) + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } - if (m10 < 0) throw UnsafeNumber + if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -465,25 +454,24 @@ object UnsafeNumbers { else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { + if (hiM10 eq null) { var x: Double = - if (e10 == 0) m10.toDouble + if (e10 == 0) loM10.toDouble else { - if (m10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - digits) { + if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { val pow10 = pow10Doubles - if (e10 < 0) m10 / pow10(-e10) - else if (e10 <= 22) m10 * pow10(e10) + if (e10 < 0) loM10 / pow10(-e10) + else if (e10 <= 22) loM10 * pow10(e10) else { - val slop = 16 - digits - (m10 * pow10(slop)) * pow10(e10 - slop) + val slop = 16 - loDigits + (loM10 * pow10(slop)) * pow10(e10 - slop) } - } else toDouble(m10, e10) + } else toDouble(loM10, e10) } if (negate) x = -x return x } - if (negate) bigM10 = bigM10.negate - new java.math.BigDecimal(bigM10, -e10).doubleValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue() } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -542,9 +530,6 @@ object UnsafeNumbers { xh * yh + (t >>> 32) + (xl * yh + (t & 0xffffffffL) >>> 32) } - private[this] final val bigIntegers: Array[java.math.BigInteger] = - (0L to 9L).map(java.math.BigInteger.valueOf).toArray - private[this] final val pow10Doubles: Array[Double] = Array(1, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22) diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index a90ce7008..11d4b1bd5 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -173,13 +173,7 @@ object UnsafeNumbers { loM10 = loM10 * 10 + (current - '0') loDigits += 1 if (loM10 >= 100000000000000000L) { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) loM10 = 0 loDigits = 0 } @@ -195,13 +189,7 @@ object UnsafeNumbers { loDigits += 1 e10 -= 1 if (loM10 >= 100000000000000000L) { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) loM10 = 0 loDigits = 0 } @@ -234,11 +222,27 @@ object UnsafeNumbers { if (negate) loM10 = -loM10 return java.math.BigDecimal.valueOf(loM10, -e10) } - hiM10 = hiM10.scaleByPowerOfTen(loDigits + e10) - if (loDigits != 0) { - if (negate) loM10 = -loM10 - hiM10 = hiM10.add(java.math.BigDecimal.valueOf(loM10, -e10)) - } + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) + } + + private[this] def toBigDecimal( + hi: java.math.BigDecimal, + lo: Long, + loDigits: Int, + e10: Int, + max_bits: Int, + negate: Boolean + ): java.math.BigDecimal = { + var loM10 = lo + if (negate) loM10 = -loM10 + val bd = + if (loDigits != 0) java.math.BigDecimal.valueOf(loM10, -e10) + else java.math.BigDecimal.ZERO + if (hi eq null) return bd + var hiM10 = hi + val scale = loDigits + e10 + if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) + hiM10 = hiM10.add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber hiM10 } @@ -264,25 +268,22 @@ object UnsafeNumbers { readAll(in, "nfinity", consume) return if (negate) Float.NegativeInfinity else Float.PositiveInfinity } - var digits = 1 // calculate digits for m10 only - var m10 = -1L - var bigM10: java.math.BigInteger = null + var loM10 = 0L + var loDigits = 0 + var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { - m10 = (current - '0').toLong + loM10 = (current - '0').toLong + loDigits += 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = m10 * 10 + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } @@ -292,21 +293,17 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 e10 -= 1 - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = m10 * 10 + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } - if (m10 < 0) throw UnsafeNumber + if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -329,21 +326,20 @@ object UnsafeNumbers { else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { + if (hiM10 eq null) { var x: Float = - if (e10 == 0) m10.toFloat + if (e10 == 0) loM10.toFloat else { - if (m10 < 4294967296L && e10 >= digits - 23 && e10 <= 19 - digits) { + if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { val pow10 = pow10Doubles - (if (e10 < 0) m10 / pow10(-e10) - else m10 * pow10(e10)).toFloat - } else toFloat(m10, e10) + (if (e10 < 0) loM10 / pow10(-e10) + else loM10 * pow10(e10)).toFloat + } else toFloat(loM10, e10) } if (negate) x = -x return x } - if (negate) bigM10 = bigM10.negate - new java.math.BigDecimal(bigM10, -e10).floatValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue() } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -400,25 +396,22 @@ object UnsafeNumbers { readAll(in, "nfinity", consume) return if (negate) Double.NegativeInfinity else Double.PositiveInfinity } - var digits = 1 // calculate digits for m10 only - var m10 = -1L - var bigM10: java.math.BigInteger = null + var loM10 = 0L + var loDigits = 0 + var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { - m10 = (current - '0').toLong + loM10 = (current - '0').toLong + loDigits += 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = m10 * 10 + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } @@ -428,21 +421,17 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 e10 -= 1 - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = m10 * 10 + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } - if (m10 < 0) throw UnsafeNumber + if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -465,25 +454,24 @@ object UnsafeNumbers { else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { + if (hiM10 eq null) { var x: Double = - if (e10 == 0) m10.toDouble + if (e10 == 0) loM10.toDouble else { - if (m10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - digits) { + if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { val pow10 = pow10Doubles - if (e10 < 0) m10 / pow10(-e10) - else if (e10 <= 22) m10 * pow10(e10) + if (e10 < 0) loM10 / pow10(-e10) + else if (e10 <= 22) loM10 * pow10(e10) else { - val slop = 16 - digits - (m10 * pow10(slop)) * pow10(e10 - slop) + val slop = 16 - loDigits + (loM10 * pow10(slop)) * pow10(e10 - slop) } - } else toDouble(m10, e10) + } else toDouble(loM10, e10) } if (negate) x = -x return x } - if (negate) bigM10 = bigM10.negate - new java.math.BigDecimal(bigM10, -e10).doubleValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue() } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -533,9 +521,6 @@ object UnsafeNumbers { @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = Math.multiplyHigh(x, y) + x + y // FIXME: Use Math.unsignedMultiplyHigh after dropping of JDK 17 support - private[this] final val bigIntegers: Array[java.math.BigInteger] = - (0L to 9L).map(java.math.BigInteger.valueOf).toArray - private[this] final val pow10Doubles: Array[Double] = Array(1, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22) diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index f831cd724..0a6e822fb 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -173,13 +173,7 @@ object UnsafeNumbers { loM10 = loM10 * 10 + (current - '0') loDigits += 1 if (loM10 >= 100000000000000000L) { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) loM10 = 0 loDigits = 0 } @@ -195,13 +189,7 @@ object UnsafeNumbers { loDigits += 1 e10 -= 1 if (loM10 >= 100000000000000000L) { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) loM10 = 0 loDigits = 0 } @@ -234,11 +222,27 @@ object UnsafeNumbers { if (negate) loM10 = -loM10 return java.math.BigDecimal.valueOf(loM10, -e10) } - hiM10 = hiM10.scaleByPowerOfTen(loDigits + e10) - if (loDigits != 0) { - if (negate) loM10 = -loM10 - hiM10 = hiM10.add(java.math.BigDecimal.valueOf(loM10, -e10)) - } + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) + } + + private[this] def toBigDecimal( + hi: java.math.BigDecimal, + lo: Long, + loDigits: Int, + e10: Int, + max_bits: Int, + negate: Boolean + ): java.math.BigDecimal = { + var loM10 = lo + if (negate) loM10 = -loM10 + val bd = + if (loDigits != 0) java.math.BigDecimal.valueOf(loM10, -e10) + else java.math.BigDecimal.ZERO + if (hi eq null) return bd + var hiM10 = hi + val scale = loDigits + e10 + if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) + hiM10 = hiM10.add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber hiM10 } @@ -264,25 +268,22 @@ object UnsafeNumbers { readAll(in, "nfinity", consume) return if (negate) Float.NegativeInfinity else Float.PositiveInfinity } - var digits = 1 // calculate digits for m10 only - var m10 = -1L - var bigM10: java.math.BigInteger = null + var loM10 = 0L + var loDigits = 0 + var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { - m10 = (current - '0').toLong + loM10 = (current - '0').toLong + loDigits += 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = m10 * 10 + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } @@ -292,21 +293,17 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 e10 -= 1 - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = m10 * 10 + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } - if (m10 < 0) throw UnsafeNumber + if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -329,21 +326,20 @@ object UnsafeNumbers { else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { + if (hiM10 eq null) { var x: Float = - if (e10 == 0) m10.toFloat + if (e10 == 0) loM10.toFloat else { - if (m10 < 4294967296L && e10 >= digits - 23 && e10 <= 19 - digits) { + if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { val pow10 = pow10Doubles - (if (e10 < 0) m10 / pow10(-e10) - else m10 * pow10(e10)).toFloat - } else toFloat(m10, e10) + (if (e10 < 0) loM10 / pow10(-e10) + else loM10 * pow10(e10)).toFloat + } else toFloat(loM10, e10) } if (negate) x = -x return x } - if (negate) bigM10 = bigM10.negate - new java.math.BigDecimal(bigM10, -e10).floatValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue() } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -400,25 +396,22 @@ object UnsafeNumbers { readAll(in, "nfinity", consume) return if (negate) Double.NegativeInfinity else Double.PositiveInfinity } - var digits = 1 // calculate digits for m10 only - var m10 = -1L - var bigM10: java.math.BigInteger = null + var loM10 = 0L + var loDigits = 0 + var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { - m10 = (current - '0').toLong + loM10 = (current - '0').toLong + loDigits += 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = m10 * 10 + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } @@ -428,21 +421,17 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 e10 -= 1 - if (m10 < 922337203685477580L) { - if (m10 <= 0) m10 = (current - '0').toLong - else { - m10 = m10 * 10 + (current - '0') - digits += 1 - } - } else { - if (bigM10 eq null) bigM10 = java.math.BigInteger.valueOf(m10) - bigM10 = bigM10.multiply(java.math.BigInteger.TEN).add(bigIntegers(current - '0')) - if (bigM10.bitLength >= max_bits) throw UnsafeNumber + if (loM10 >= 100000000000000000L) { + hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + loM10 = 0 + loDigits = 0 } } } - if (m10 < 0) throw UnsafeNumber + if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -465,25 +454,24 @@ object UnsafeNumbers { else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber - if (bigM10 eq null) { + if (hiM10 eq null) { var x: Double = - if (e10 == 0) m10.toDouble + if (e10 == 0) loM10.toDouble else { - if (m10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - digits) { + if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { val pow10 = pow10Doubles - if (e10 < 0) m10 / pow10(-e10) - else if (e10 <= 22) m10 * pow10(e10) + if (e10 < 0) loM10 / pow10(-e10) + else if (e10 <= 22) loM10 * pow10(e10) else { - val slop = 16 - digits - (m10 * pow10(slop)) * pow10(e10 - slop) + val slop = 16 - loDigits + (loM10 * pow10(slop)) * pow10(e10 - slop) } - } else toDouble(m10, e10) + } else toDouble(loM10, e10) } if (negate) x = -x return x } - if (negate) bigM10 = bigM10.negate - new java.math.BigDecimal(bigM10, -e10).doubleValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue() } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -530,9 +518,6 @@ object UnsafeNumbers { if (consume && current != -1) throw UnsafeNumber } - private[this] final val bigIntegers: Array[java.math.BigInteger] = - (0L to 9L).map(java.math.BigInteger.valueOf).toArray - private[this] final val pow10Doubles: Array[Double] = Array(1, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22) From 5c1b9ae324f39428132e71c5c25fffb5b63a8dff Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 2 Feb 2025 16:36:44 +0100 Subject: [PATCH 132/311] More efficient decoding of arrays (#1278) --- .../zio/json/internal/UnsafeNumbers.scala | 4 +- .../zio/json/internal/UnsafeNumbers.scala | 6 +- .../src/main/scala/zio/json/JsonDecoder.scala | 170 +++++++++--------- .../main/scala/zio/json/internal/lexer.scala | 4 +- 4 files changed, 90 insertions(+), 94 deletions(-) diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 11d4b1bd5..a41908157 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -225,7 +225,7 @@ object UnsafeNumbers { toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } - private[this] def toBigDecimal( + @noinline private[this] def toBigDecimal( hi: java.math.BigDecimal, lo: Long, loDigits: Int, @@ -476,7 +476,7 @@ object UnsafeNumbers { // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct - private[this] def toDouble(m10: Long, e10: Int): Double = + @inline private[this] def toDouble(m10: Long, e10: Int): Double = if (m10 == 0 || e10 < -343) 0.0 else if (e10 >= 310) Double.PositiveInfinity else { diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 0a6e822fb..3b9b9f2df 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -225,7 +225,7 @@ object UnsafeNumbers { toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } - private[this] def toBigDecimal( + @noinline private[this] def toBigDecimal( hi: java.math.BigDecimal, lo: Long, loDigits: Int, @@ -344,7 +344,7 @@ object UnsafeNumbers { // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct - private[this] def toFloat(m10: Long, e10: Int): Float = + @noinline private[this] def toFloat(m10: Long, e10: Int): Float = if (m10 == 0 || e10 < -64) 0.0f else if (e10 >= 39) Float.PositiveInfinity else { @@ -476,7 +476,7 @@ object UnsafeNumbers { // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct - private[this] def toDouble(m10: Long, e10: Int): Double = + @inline private[this] def toDouble(m10: Long, e10: Int): Double = if (m10 == 0 || e10 < -343) 0.0 else if (e10 >= 310) Double.PositiveInfinity else { diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index b36f5b9bc..8541bb27a 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -58,14 +58,11 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { */ final def <*[B](that: => JsonDecoder[B]): JsonDecoder[A] = self.zipLeft(that) - final def both[B](that: => JsonDecoder[B]): JsonDecoder[(A, B)] = - bothWith(that)((a, b) => (a, b)) + final def both[B](that: => JsonDecoder[B]): JsonDecoder[(A, B)] = bothWith(that)((a, b) => (a, b)) - final def bothRight[B](that: => JsonDecoder[B]): JsonDecoder[B] = - bothWith(that)((_, b) => b) + final def bothRight[B](that: => JsonDecoder[B]): JsonDecoder[B] = bothWith(that)((_, b) => b) - final def bothLeft[B](that: => JsonDecoder[B]): JsonDecoder[A] = - bothWith(that)((a, _) => a) + final def bothLeft[B](that: => JsonDecoder[B]): JsonDecoder[A] = bothWith(that)((a, _) => a) final def bothWith[B, C](that: => JsonDecoder[B])(f: (A, B) => C): JsonDecoder[C] = new JsonDecoder[C] { @@ -113,7 +110,6 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { */ final def orElse[A1 >: A](that: => JsonDecoder[A1]): JsonDecoder[A1] = new JsonDecoder[A1] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): A1 = { val rr = RecordingReader(in) try self.unsafeDecode(trace, rr) @@ -151,14 +147,13 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { new MappedJsonDecoder[B] { private[json] def underlying: JsonDecoder[A] = self - def unsafeDecode(trace: List[JsonError], in: RetractReader): B = - f(self.unsafeDecode(trace, in)) + def unsafeDecode(trace: List[JsonError], in: RetractReader): B = f(self.unsafeDecode(trace, in)) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): B = - f(self.unsafeFromJsonAST(trace, json)) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): B = f( + self.unsafeFromJsonAST(trace, json) + ) - override def unsafeDecodeMissing(trace: List[JsonError]): B = - f(self.unsafeDecodeMissing(trace)) + override def unsafeDecodeMissing(trace: List[JsonError]): B = f(self.unsafeDecodeMissing(trace)) } /** @@ -167,7 +162,6 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { */ final def mapOrFail[B](f: A => Either[String, B]): JsonDecoder[B] = new JsonDecoder[B] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): B = f(self.unsafeDecode(trace, in)) match { case Right(b) => b @@ -206,11 +200,9 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { /** * Zips two codecs into one, transforming the outputs of zip codecs by the specified function. */ - final def zipWith[B, C](that: => JsonDecoder[B])(f: (A, B) => C): JsonDecoder[C] = - self.zip(that).map(f.tupled) + final def zipWith[B, C](that: => JsonDecoder[B])(f: (A, B) => C): JsonDecoder[C] = self.zip(that).map(f.tupled) - def unsafeDecodeMissing(trace: List[JsonError]): A = - Lexer.error("missing", trace) + def unsafeDecodeMissing(trace: List[JsonError]): A = Lexer.error("missing", trace) /** * Low-level, unsafe method to decode a value or throw an exception. This method should not be called in application @@ -252,7 +244,6 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with with NoStackTrace def peekChar[A](partialFunction: PartialFunction[Char, JsonDecoder[A]]): JsonDecoder[A] = new JsonDecoder[A] { - override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val c = in.nextNonWhitespace() if (partialFunction.isDefinedAt(c)) { @@ -266,8 +257,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with new JsonDecoder[A] { lazy val decoder = decoder0 - override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = - decoder.unsafeDecode(trace, in) + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = decoder.unsafeDecode(trace, in) override def unsafeDecodeMissing(trace: List[JsonError]): A = decoder.unsafeDecodeMissing(trace) @@ -275,9 +265,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with } implicit val string: JsonDecoder[String] = new JsonDecoder[String] { - - def unsafeDecode(trace: List[JsonError], in: RetractReader): String = - Lexer.string(trace, in).toString + def unsafeDecode(trace: List[JsonError], in: RetractReader): String = Lexer.string(trace, in).toString override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): String = json match { @@ -287,9 +275,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with } implicit val boolean: JsonDecoder[Boolean] = new JsonDecoder[Boolean] { - - def unsafeDecode(trace: List[JsonError], in: RetractReader): Boolean = - Lexer.boolean(trace, in) + def unsafeDecode(trace: List[JsonError], in: RetractReader): Boolean = Lexer.boolean(trace, in) override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Boolean = json match { @@ -310,40 +296,36 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with implicit val symbol: JsonDecoder[Symbol] = string.map(Symbol(_)) - implicit val byte: JsonDecoder[Byte] = number(Lexer.byte, _.byteValueExact()) - implicit val short: JsonDecoder[Short] = number(Lexer.short, _.shortValueExact()) - implicit val int: JsonDecoder[Int] = number(Lexer.int, _.intValueExact()) - implicit val long: JsonDecoder[Long] = number(Lexer.long, _.longValueExact()) + implicit val byte: JsonDecoder[Byte] = number(Lexer.byte, _.byteValueExact) + implicit val short: JsonDecoder[Short] = number(Lexer.short, _.shortValueExact) + implicit val int: JsonDecoder[Int] = number(Lexer.int, _.intValueExact) + implicit val long: JsonDecoder[Long] = number(Lexer.long, _.longValueExact) implicit val bigInteger: JsonDecoder[java.math.BigInteger] = number(Lexer.bigInteger, _.toBigIntegerExact) implicit val scalaBigInt: JsonDecoder[BigInt] = number(Lexer.bigInteger, _.toBigIntegerExact) - implicit val float: JsonDecoder[Float] = number(Lexer.float, _.floatValue()) - implicit val double: JsonDecoder[Double] = number(Lexer.double, _.doubleValue()) + implicit val float: JsonDecoder[Float] = number(Lexer.float, _.floatValue) + implicit val double: JsonDecoder[Double] = number(Lexer.double, _.doubleValue) implicit val bigDecimal: JsonDecoder[java.math.BigDecimal] = number(Lexer.bigDecimal, identity) implicit val scalaBigDecimal: JsonDecoder[BigDecimal] = - number(Lexer.bigDecimal, x => new BigDecimal(x, BigDecimal.defaultMathContext)) + number(Lexer.bigDecimal, new BigDecimal(_, BigDecimal.defaultMathContext)) // numbers decode from numbers or strings for maximum compatibility - private[this] def number[A]( - f: (List[JsonError], RetractReader) => A, - fromBigDecimal: java.math.BigDecimal => A - ): JsonDecoder[A] = + private[this] def number[A](f: (List[JsonError], RetractReader) => A, g: java.math.BigDecimal => A): JsonDecoder[A] = new JsonDecoder[A] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = - if (in.nextNonWhitespace() == '"') { + if (in.nextNonWhitespace() != '"') { + in.retract() + f(trace, in) + } else { val a = f(trace, in) val c = in.readChar() if (c != '"') Lexer.error("'\"'", c, trace) a - } else { - in.retract() - f(trace, in) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case Json.Num(value) => - try fromBigDecimal(value) + try g(value) catch { case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) } @@ -358,7 +340,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with // use a newtype wrapper. implicit def option[A](implicit A: JsonDecoder[A]): JsonDecoder[Option[A]] = - new OptionJsonDecoder[Option[A]] { self => + new OptionJsonDecoder[Option[A]] { override def unsafeDecodeMissing(trace: List[JsonError]): Option[A] = None def unsafeDecode(trace: List[JsonError], in: RetractReader): Option[A] = @@ -380,38 +362,32 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with // supports multiple representations for compatibility with other libraries, // but does not support the "discriminator field" encoding with a field named // "value" used by some libraries. - implicit def either[A, B](implicit - A: JsonDecoder[A], - B: JsonDecoder[B] - ): JsonDecoder[Either[A, B]] = + implicit def either[A, B](implicit A: JsonDecoder[A], B: JsonDecoder[B]): JsonDecoder[Either[A, B]] = new JsonDecoder[Either[A, B]] { private[this] val names = Array("a", "Left", "left", "b", "Right", "right") private[this] val matrix = new StringMatrix(names) - private[this] val spans = names.map(JsonError.ObjectAccess(_)) + private[this] val spans = names.map(new JsonError.ObjectAccess(_)) - def unsafeDecode( - trace: List[JsonError], - in: RetractReader - ): Either[A, B] = { - Lexer.char(trace, in, '{') + def unsafeDecode(trace: List[JsonError], in: RetractReader): Either[A, B] = { + val c = in.nextNonWhitespace() + if (c != '{') Lexer.error("'{'", c, trace) var left: Any = null var right: Any = null if (Lexer.firstField(trace, in)) while ({ - { - val field = Lexer.field(trace, in, matrix) - if (field == -1) Lexer.skipValue(trace, in) - else { - val trace_ = spans(field) :: trace - if (field < 3) { - if (left != null) Lexer.error("duplicate", trace_) - left = A.unsafeDecode(trace_, in) - } else { - if (right != null) Lexer.error("duplicate", trace_) - right = B.unsafeDecode(trace_, in) - } + val field = Lexer.field(trace, in, matrix) + if (field == -1) Lexer.skipValue(trace, in) + else { + val trace_ = spans(field) :: trace + if (field < 3) { + if (left != null) Lexer.error("duplicate", trace_) + left = A.unsafeDecode(trace_, in) + } else { + if (right != null) Lexer.error("duplicate", trace_) + right = B.unsafeDecode(trace_, in) } - }; Lexer.nextField(trace, in) + } + Lexer.nextField(trace, in) }) () if (left == null && right == null) Lexer.error("missing fields", trace) if (left != null && right != null) Lexer.error("ambiguous either, zip present", trace) @@ -420,7 +396,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with } } - private[json] def builder[A, T[_]]( + @inline private[json] def builder[A, T[_]]( trace: List[JsonError], in: RetractReader, builder: mutable.Builder[A, T[A]] @@ -429,25 +405,26 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with if (c != '[') Lexer.error("'['", c, trace) var i: Int = 0 if (Lexer.firstArrayElement(in)) while ({ - builder += A.unsafeDecode(JsonError.ArrayAccess(i) :: trace, in) + builder += A.unsafeDecode(new JsonError.ArrayAccess(i) :: trace, in) i += 1 Lexer.nextArrayElement(trace, in) }) () builder.result() } - private[json] def keyValueBuilder[K, V, T[X, Y] <: Iterable[(X, Y)]]( + @inline private[json] def keyValueBuilder[K, V, T[X, Y] <: Iterable[(X, Y)]]( trace: List[JsonError], in: RetractReader, builder: mutable.Builder[(K, V), T[K, V]] )(implicit K: JsonFieldDecoder[K], V: JsonDecoder[V]): T[K, V] = { - val c = in.nextNonWhitespace() + var c = in.nextNonWhitespace() if (c != '{') Lexer.error("'{'", c, trace) if (Lexer.firstField(trace, in)) while ({ val field = Lexer.string(trace, in).toString - val trace_ = JsonError.ObjectAccess(field) :: trace - Lexer.char(trace_, in, ':') + val trace_ = new JsonError.ObjectAccess(field) :: trace + c = in.nextNonWhitespace() + if (c != ':') Lexer.error("':'", c, trace) val value = V.unsafeDecode(trace_, in) builder += ((K.unsafeDecodeField(trace_, field), value)) Lexer.nextField(trace, in) @@ -455,7 +432,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with builder.result() } - // use this instead of `string.mapOrFail` in supertypes (to prevent class initialization error at runtime) + // FIXME: remove in the next major version private[json] def mapStringOrFail[A](f: String => Either[String, A]): JsonDecoder[A] = new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = @@ -481,18 +458,40 @@ private[json] trait MappedJsonDecoder[A] extends JsonDecoder[A] { private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { this: JsonDecoder.type => - implicit def array[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[Array[A]] = + implicit def array[A](implicit A: JsonDecoder[A], ct: reflect.ClassTag[A]): JsonDecoder[Array[A]] = new CollectionJsonDecoder[Array[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Array[A] = Array.empty - def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = - builder(trace, in, Array.newBuilder[A]) + def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = { + val c = in.nextNonWhitespace() + if (c != '[') Lexer.error("'['", c, trace) + if (Lexer.firstArrayElement(in)) { + var l = 8 + var x = new Array[A](l) + var i = 0 + while ({ + if (i == l) { + l <<= 1 + val x1 = new Array[A](l) + System.arraycopy(x, 0, x1, 0, i) + x = x1 + } + x(i) = A.unsafeDecode(new JsonError.ArrayAccess(i) :: trace, in) + i += 1 + Lexer.nextArrayElement(trace, in) + }) () + if (i != l) { + val x1 = new Array[A](i) + _root_.java.lang.System.arraycopy(x, 0, x1, 0, i) + x = x1 + } + x + } else Array.empty + } } implicit def seq[A: JsonDecoder]: JsonDecoder[Seq[A]] = new CollectionJsonDecoder[Seq[A]] { - override def unsafeDecodeMissing(trace: List[JsonError]): Seq[A] = Seq.empty def unsafeDecode(trace: List[JsonError], in: RetractReader): Seq[A] = @@ -514,7 +513,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { elements.map { var i = 0 json => - val span = JsonError.ArrayAccess(i) + val span = new JsonError.ArrayAccess(i) i += 1 decoder.unsafeFromJsonAST(span :: trace, json) } @@ -661,10 +660,7 @@ private[json] trait DecoderLowPriority2 extends DecoderLowPriority3 { } // not implicit because this overlaps with decoders for lists of tuples - def keyValueChunk[K, A](implicit - K: JsonFieldDecoder[K], - A: JsonDecoder[A] - ): JsonDecoder[Chunk[(K, A)]] = + def keyValueChunk[K, A](implicit K: JsonFieldDecoder[K], A: JsonDecoder[A]): JsonDecoder[Chunk[(K, A)]] = new CollectionJsonDecoder[Chunk[(K, A)]] { override def unsafeDecodeMissing(trace: List[JsonError]): Chunk[(K, A)] = Chunk.empty @@ -680,7 +676,7 @@ private[json] trait DecoderLowPriority2 extends DecoderLowPriority3 { private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { this: JsonDecoder.type => - import java.time.{ DateTimeException, _ } + import java.time._ implicit val dayOfWeek: JsonDecoder[DayOfWeek] = javaTimeDecoder(s => DayOfWeek.valueOf(s.toUpperCase)) implicit val duration: JsonDecoder[Duration] = javaTimeDecoder(parsers.unsafeParseDuration) @@ -720,7 +716,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } - // Commonized handling for decoding from string to java.time Class + // FIXME: remove in the next major version private[json] def parseJavaTime[A](f: String => A, s: String): Either[String, A] = try new Right(f(s)) catch { diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 5a27e318f..5aeefdcc7 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -56,13 +56,13 @@ object Lexer { } // True if we got anything besides a ], False for ] - def firstArrayElement(in: RetractReader): Boolean = + @inline def firstArrayElement(in: RetractReader): Boolean = in.nextNonWhitespace() != ']' && { in.retract() true } - def nextArrayElement(trace: List[JsonError], in: OneCharReader): Boolean = + @inline def nextArrayElement(trace: List[JsonError], in: OneCharReader): Boolean = (in.nextNonWhitespace(): @switch) match { case ',' => true case ']' => false From fec8e2c1a69906662feafaab5bdcf8f907b75197 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 2 Feb 2025 19:46:33 +0100 Subject: [PATCH 133/311] Add support of `immutable.ArraySeq` + more inlinings (#1279) --- .../zio/json}/JsonCodecVersionSpecific.scala | 0 .../zio/json/JsonDecoderVersionSpecific.scala | 0 .../zio/json/JsonEncoderVersionSpecific.scala | 0 .../zio/json/JsonCodecVersionSpecific.scala | 8 ++ .../zio/json/JsonDecoderVersionSpecific.scala | 20 +++++ .../zio/json/JsonEncoderVersionSpecific.scala | 23 +++++ .../zio/json/JsonCodecVersionSpecific.scala | 4 + .../zio/json/JsonDecoderVersionSpecific.scala | 15 +++- .../zio/json/JsonEncoderVersionSpecific.scala | 18 +++- .../src/main/scala/zio/json/JsonCodec.scala | 2 + .../src/main/scala/zio/json/JsonDecoder.scala | 2 +- .../src/main/scala/zio/json/JsonEncoder.scala | 85 ++++++------------- .../main/scala/zio/json/internal/lexer.scala | 28 ++---- .../zio/json/CodecVersionSpecificSpec.scala | 18 ++++ .../zio/json/DecoderVersionSpecificSpec.scala | 30 +++++++ .../zio/json/EncoderVesionSpecificSpec.scala | 31 +++++++ .../zio/json/CodecVersionSpecificSpec.scala | 18 ++++ .../zio/json/DecoderVersionSpecificSpec.scala | 30 +++++++ .../zio/json/EncoderVesionSpecificSpec.scala | 31 +++++++ 19 files changed, 282 insertions(+), 81 deletions(-) rename zio-json/shared/src/main/{scala-2.x => scala-2.12/zio/json}/JsonCodecVersionSpecific.scala (100%) rename zio-json/shared/src/main/{scala-2.x => scala-2.12}/zio/json/JsonDecoderVersionSpecific.scala (100%) rename zio-json/shared/src/main/{scala-2.x => scala-2.12}/zio/json/JsonEncoderVersionSpecific.scala (100%) create mode 100644 zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala create mode 100644 zio-json/shared/src/main/scala-2.13/zio/json/JsonDecoderVersionSpecific.scala create mode 100644 zio-json/shared/src/main/scala-2.13/zio/json/JsonEncoderVersionSpecific.scala create mode 100644 zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala create mode 100644 zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala create mode 100644 zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala create mode 100644 zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala create mode 100644 zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala create mode 100644 zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala diff --git a/zio-json/shared/src/main/scala-2.x/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala similarity index 100% rename from zio-json/shared/src/main/scala-2.x/JsonCodecVersionSpecific.scala rename to zio-json/shared/src/main/scala-2.12/zio/json/JsonCodecVersionSpecific.scala diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-2.12/zio/json/JsonDecoderVersionSpecific.scala similarity index 100% rename from zio-json/shared/src/main/scala-2.x/zio/json/JsonDecoderVersionSpecific.scala rename to zio-json/shared/src/main/scala-2.12/zio/json/JsonDecoderVersionSpecific.scala diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-2.12/zio/json/JsonEncoderVersionSpecific.scala similarity index 100% rename from zio-json/shared/src/main/scala-2.x/zio/json/JsonEncoderVersionSpecific.scala rename to zio-json/shared/src/main/scala-2.12/zio/json/JsonEncoderVersionSpecific.scala diff --git a/zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala new file mode 100644 index 000000000..e6d8f0f9b --- /dev/null +++ b/zio-json/shared/src/main/scala-2.13/zio/json/JsonCodecVersionSpecific.scala @@ -0,0 +1,8 @@ +package zio.json + +import scala.collection.immutable + +trait JsonCodecVersionSpecific { + implicit def arraySeq[A: JsonEncoder: JsonDecoder: reflect.ClassTag]: JsonCodec[immutable.ArraySeq[A]] = + JsonCodec(JsonEncoder.arraySeq[A], JsonDecoder.arraySeq[A]) +} diff --git a/zio-json/shared/src/main/scala-2.13/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-2.13/zio/json/JsonDecoderVersionSpecific.scala new file mode 100644 index 000000000..eb0ffc9e0 --- /dev/null +++ b/zio-json/shared/src/main/scala-2.13/zio/json/JsonDecoderVersionSpecific.scala @@ -0,0 +1,20 @@ +package zio.json + +import zio.json.JsonDecoder.JsonError +import zio.json.internal.RetractReader + +import scala.collection.immutable + +private[json] trait JsonDecoderVersionSpecific { + implicit def arraySeq[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[immutable.ArraySeq[A]] = + new CollectionJsonDecoder[immutable.ArraySeq[A]] { + private[this] val arrayDecoder = JsonDecoder.array[A] + + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ArraySeq[A] = immutable.ArraySeq.empty + + def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.ArraySeq[A] = + immutable.ArraySeq.unsafeWrapArray(arrayDecoder.unsafeDecode(trace, in)) + } +} + +private[json] trait DecoderLowPriorityVersionSpecific diff --git a/zio-json/shared/src/main/scala-2.13/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-2.13/zio/json/JsonEncoderVersionSpecific.scala new file mode 100644 index 000000000..693472327 --- /dev/null +++ b/zio-json/shared/src/main/scala-2.13/zio/json/JsonEncoderVersionSpecific.scala @@ -0,0 +1,23 @@ +package zio.json + +import zio.json.ast.Json +import zio.json.internal.Write + +import scala.collection.immutable + +private[json] trait JsonEncoderVersionSpecific { + implicit def arraySeq[A: JsonEncoder: scala.reflect.ClassTag]: JsonEncoder[immutable.ArraySeq[A]] = + new JsonEncoder[immutable.ArraySeq[A]] { + private[this] val arrayEnc = JsonEncoder.array[A] + + override def isEmpty(as: immutable.ArraySeq[A]): Boolean = as.isEmpty + + def unsafeEncode(as: immutable.ArraySeq[A], indent: Option[Int], out: Write): Unit = + arrayEnc.unsafeEncode(as.unsafeArray.asInstanceOf[Array[A]], indent, out) + + override final def toJsonAST(as: immutable.ArraySeq[A]): Either[String, Json] = + arrayEnc.toJsonAST(as.unsafeArray.asInstanceOf[Array[A]]) + } +} + +private[json] trait EncoderLowPriorityVersionSpecific diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala index f9c180f0a..0be993585 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala @@ -1,6 +1,10 @@ package zio.json +import scala.collection.immutable + private[json] trait JsonCodecVersionSpecific { inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonCodec[A] = DeriveJsonCodec.gen[A] + implicit def arraySeq[A: JsonEncoder: JsonDecoder: reflect.ClassTag]: JsonCodec[immutable.ArraySeq[A]] = + JsonCodec(JsonEncoder.arraySeq[A], JsonDecoder.arraySeq[A]) } diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala index a70233ec5..4cb0edb13 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala @@ -1,15 +1,28 @@ package zio.json +import zio.json.JsonDecoder.JsonError +import zio.json.internal.RetractReader + +import scala.collection.immutable import scala.compiletime.* import scala.compiletime.ops.any.IsConst private[json] trait JsonDecoderVersionSpecific { inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonDecoder[A] = DeriveJsonDecoder.gen[A] + + implicit def arraySeq[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[immutable.ArraySeq[A]] = + new CollectionJsonDecoder[immutable.ArraySeq[A]] { + private[this] val arrayDecoder = JsonDecoder.array[A] + + override def unsafeDecodeMissing(trace: List[JsonError]): immutable.ArraySeq[A] = immutable.ArraySeq.empty + + def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.ArraySeq[A] = + immutable.ArraySeq.unsafeWrapArray(arrayDecoder.unsafeDecode(trace, in)) + } } trait DecoderLowPriorityVersionSpecific { - inline given unionOfStringEnumeration[T](using IsUnionOf[String, T]): JsonDecoder[T] = val values = UnionDerivation.constValueUnionTuple[String, T] JsonDecoder.string.mapOrFail { diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala index 82932a7cf..e9b290068 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala @@ -1,14 +1,30 @@ package zio.json +import zio.json.ast.Json +import zio.json.internal.Write + +import scala.collection.immutable import scala.compiletime.ops.any.IsConst private[json] trait JsonEncoderVersionSpecific { inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonEncoder[A] = DeriveJsonEncoder.gen[A] + + implicit def arraySeq[A: JsonEncoder: scala.reflect.ClassTag]: JsonEncoder[immutable.ArraySeq[A]] = + new JsonEncoder[immutable.ArraySeq[A]] { + private[this] val arrayEnc = JsonEncoder.array[A] + + override def isEmpty(as: immutable.ArraySeq[A]): Boolean = as.isEmpty + + def unsafeEncode(as: immutable.ArraySeq[A], indent: Option[Int], out: Write): Unit = + arrayEnc.unsafeEncode(as.unsafeArray.asInstanceOf[Array[A]], indent, out) + + override final def toJsonAST(as: immutable.ArraySeq[A]): Either[String, Json] = + arrayEnc.toJsonAST(as.unsafeArray.asInstanceOf[Array[A]]) + } } private[json] trait EncoderLowPriorityVersionSpecific { - inline given unionOfStringEnumeration[T](using IsUnionOf[String, T]): JsonEncoder[T] = JsonEncoder.string.asInstanceOf[JsonEncoder[T]] } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala index 25d368f73..ccd6ece13 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala @@ -120,6 +120,8 @@ object JsonCodec extends GeneratedTupleCodecs with CodecLowPriority0 with JsonCo } private[json] trait CodecLowPriority0 extends CodecLowPriority1 { this: JsonCodec.type => + implicit def array[A: JsonEncoder: JsonDecoder: reflect.ClassTag]: JsonCodec[Array[A]] = + JsonCodec(JsonEncoder.array[A], JsonDecoder.array[A]) implicit def chunk[A: JsonEncoder: JsonDecoder]: JsonCodec[Chunk[A]] = JsonCodec(JsonEncoder.chunk[A], JsonDecoder.chunk[A]) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 8541bb27a..e6183691d 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -458,7 +458,7 @@ private[json] trait MappedJsonDecoder[A] extends JsonDecoder[A] { private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { this: JsonDecoder.type => - implicit def array[A](implicit A: JsonDecoder[A], ct: reflect.ClassTag[A]): JsonDecoder[Array[A]] = + implicit def array[A](implicit A: JsonDecoder[A], classTag: reflect.ClassTag[A]): JsonDecoder[Array[A]] = new CollectionJsonDecoder[Array[A]] { override def unsafeDecodeMissing(trace: List[JsonError]): Array[A] = Array.empty diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index ca09d2977..f3b0d09e7 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -23,7 +23,6 @@ import zio.{ Chunk, NonEmptyChunk } import java.util.UUID import scala.annotation._ import scala.collection.{ immutable, mutable } -import scala.reflect.ClassTag trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { self => @@ -33,7 +32,6 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { * user-defined function. */ final def contramap[B](f: B => A): JsonEncoder[B] = new JsonEncoder[B] { - override def unsafeEncode(b: B, indent: Option[Int], out: Write): Unit = self.unsafeEncode(f(b), indent, out) @@ -41,8 +39,7 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { override def isEmpty(b: B): Boolean = self.isEmpty(f(b)) - override final def toJsonAST(b: B): Either[String, Json] = - self.toJsonAST(f(b)) + override final def toJsonAST(b: B): Either[String, Json] = self.toJsonAST(f(b)) } /** @@ -114,10 +111,9 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { } object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with JsonEncoderVersionSpecific { - def apply[A](implicit a: JsonEncoder[A]): JsonEncoder[A] = a + @inline def apply[A](implicit a: JsonEncoder[A]): JsonEncoder[A] = a implicit val string: JsonEncoder[String] = new JsonEncoder[String] { - override def unsafeEncode(a: String, indent: Option[Int], out: Write): Unit = { out.write('"') val len = a.length @@ -134,8 +130,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: String): Either[String, Json] = - Right(Json.Str(a)) + override final def toJsonAST(a: String): Either[String, Json] = new Right(Json.Str(a)) private[this] def writeEncoded(a: String, out: Write): Unit = { val len = a.length @@ -157,11 +152,9 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with } out.write('"') } - } implicit val char: JsonEncoder[Char] = new JsonEncoder[Char] { - override def unsafeEncode(a: Char, indent: Option[Int], out: Write): Unit = { out.write('"') (a: @switch) match { @@ -179,15 +172,13 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: Char): Either[String, Json] = - Right(Json.Str(a.toString)) + override final def toJsonAST(a: Char): Either[String, Json] = new Right(Json.Str(a.toString)) } private[json] def explicit[A](f: A => String, g: A => Json): JsonEncoder[A] = new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = out.write(f(a)) - override final def toJsonAST(a: A): Either[String, Json] = - Right(g(a)) + override final def toJsonAST(a: A): Either[String, Json] = new Right(g(a)) } private[json] def stringify[A](f: A => String): JsonEncoder[A] = new JsonEncoder[A] { @@ -197,8 +188,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: A): Either[String, Json] = - Right(Json.Str(f(a))) + override final def toJsonAST(a: A): Either[String, Json] = new Right(Json.Str(f(a))) } def suspend[A](encoder0: => JsonEncoder[A]): JsonEncoder[A] = @@ -224,48 +214,36 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with explicit(_.toString, n => Json.Num(new java.math.BigDecimal(n))) implicit val scalaBigInt: JsonEncoder[BigInt] = explicit(_.toString, n => Json.Num(new java.math.BigDecimal(n.bigInteger))) - implicit val double: JsonEncoder[Double] = - explicit(SafeNumbers.toString, n => Json.Num(n)) - implicit val float: JsonEncoder[Float] = - explicit(SafeNumbers.toString, n => Json.Num(n)) + implicit val double: JsonEncoder[Double] = explicit(SafeNumbers.toString, n => Json.Num(n)) + implicit val float: JsonEncoder[Float] = explicit(SafeNumbers.toString, n => Json.Num(n)) implicit val bigDecimal: JsonEncoder[java.math.BigDecimal] = explicit(_.toString, Json.Num.apply) implicit val scalaBigDecimal: JsonEncoder[BigDecimal] = explicit(_.toString, n => Json.Num(n.bigDecimal)) implicit def option[A](implicit A: JsonEncoder[A]): JsonEncoder[Option[A]] = new JsonEncoder[Option[A]] { + def unsafeEncode(oa: Option[A], indent: Option[Int], out: Write): Unit = + if (oa eq None) out.write("null") + else A.unsafeEncode(oa.get, indent, out) - def unsafeEncode(oa: Option[A], indent: Option[Int], out: Write): Unit = oa match { - case None => out.write("null") - case Some(a) => A.unsafeEncode(a, indent, out) - } - - override def isNothing(oa: Option[A]): Boolean = - oa match { - case None => true - case Some(a) => A.isNothing(a) - } + override def isNothing(oa: Option[A]): Boolean = (oa eq None) || A.isNothing(oa.get) override final def toJsonAST(oa: Option[A]): Either[String, Json] = - oa match { - case None => Right(Json.Null) - case Some(a) => A.toJsonAST(a) - } + if (oa eq None) new Right(Json.Null) + else A.toJsonAST(oa.get) } - def bump(indent: Option[Int]): Option[Int] = indent match { - case None => None - case Some(i) => Some(i + 1) - } + def bump(indent: Option[Int]): Option[Int] = + if (indent ne None) new Some(indent.get + 1) + else indent - def pad(indent: Option[Int], out: Write): Unit = indent match { - case None => () - case Some(n) => + def pad(indent: Option[Int], out: Write): Unit = + if (indent ne None) { out.write('\n') - var i = n + var i = indent.get while (i > 0) { out.write(" ") i -= 1 } - } + } implicit def either[A, B](implicit A: JsonEncoder[A], B: JsonEncoder[B]): JsonEncoder[Either[A, B]] = new JsonEncoder[Either[A, B]] { @@ -320,12 +298,8 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { this: JsonEncoder.type => - implicit def array[A](implicit - A: JsonEncoder[A], - classTag: ClassTag[A] - ): JsonEncoder[Array[A]] = + implicit def array[A](implicit A: JsonEncoder[A], classTag: scala.reflect.ClassTag[A]): JsonEncoder[Array[A]] = new JsonEncoder[Array[A]] { - override def isEmpty(as: Array[A]): Boolean = as.isEmpty def unsafeEncode(as: Array[A], indent: Option[Int], out: Write): Unit = @@ -389,17 +363,14 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { implicit def vector[A: JsonEncoder]: JsonEncoder[Vector[A]] = iterable[A, Vector] - implicit def set[A: JsonEncoder]: JsonEncoder[Set[A]] = - iterable[A, Set] + implicit def set[A: JsonEncoder]: JsonEncoder[Set[A]] = iterable[A, Set] - implicit def hashSet[A: JsonEncoder]: JsonEncoder[immutable.HashSet[A]] = - iterable[A, immutable.HashSet] + implicit def hashSet[A: JsonEncoder]: JsonEncoder[immutable.HashSet[A]] = iterable[A, immutable.HashSet] implicit def sortedSet[A: Ordering: JsonEncoder]: JsonEncoder[immutable.SortedSet[A]] = iterable[A, immutable.SortedSet] - implicit def map[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[Map[K, V]] = - keyValueIterable[K, V, Map] + implicit def map[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[Map[K, V]] = keyValueIterable[K, V, Map] implicit def hashMap[K: JsonFieldEncoder, V: JsonEncoder]: JsonEncoder[immutable.HashMap[K, V]] = keyValueIterable[K, V, immutable.HashMap] @@ -417,11 +388,8 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { this: JsonEncoder.type => - implicit def iterable[A, T[X] <: Iterable[X]](implicit - A: JsonEncoder[A] - ): JsonEncoder[T[A]] = + implicit def iterable[A, T[X] <: Iterable[X]](implicit A: JsonEncoder[A]): JsonEncoder[T[A]] = new JsonEncoder[T[A]] { - override def isEmpty(as: T[A]): Boolean = as.isEmpty def unsafeEncode(as: T[A], indent: Option[Int], out: Write): Unit = @@ -471,7 +439,6 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { K: JsonFieldEncoder[K], A: JsonEncoder[A] ): JsonEncoder[T[K, A]] = new JsonEncoder[T[K, A]] { - override def isEmpty(a: T[K, A]): Boolean = a.isEmpty def unsafeEncode(kvs: T[K, A], indent: Option[Int], out: Write): Unit = diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 5aeefdcc7..787220c56 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -38,7 +38,7 @@ object Lexer { error(s"invalid '\\$c' in string", trace) // True if we got a string (implies a retraction), False for } - def firstField(trace: List[JsonError], in: RetractReader): Boolean = + @inline def firstField(trace: List[JsonError], in: RetractReader): Boolean = (in.nextNonWhitespace(): @switch) match { case '"' => in.retract() @@ -48,7 +48,7 @@ object Lexer { } // True if we got a comma, and False for } - def nextField(trace: List[JsonError], in: OneCharReader): Boolean = + @inline def nextField(trace: List[JsonError], in: OneCharReader): Boolean = (in.nextNonWhitespace(): @switch) match { case ',' => true case '}' => false @@ -69,7 +69,7 @@ object Lexer { case c => error("',' or ']'", c, trace) } - def field(trace: List[JsonError], in: OneCharReader, matrix: StringMatrix): Int = { + @inline def field(trace: List[JsonError], in: OneCharReader, matrix: StringMatrix): Int = { val f = enumeration(trace, in, matrix) val c = in.nextNonWhitespace() if (c != ':') error("':'", c, trace) @@ -106,7 +106,7 @@ object Lexer { matrix.first(bs) } - def skipValue(trace: List[JsonError], in: RetractReader): Unit = + @noinline def skipValue(trace: List[JsonError], in: RetractReader): Unit = (in.nextNonWhitespace(): @switch) match { case 'n' | 't' => skipFixedChars(in, 3) case 'f' => skipFixedChars(in, 4) @@ -120,10 +120,11 @@ object Lexer { } def skipNumber(in: RetractReader): Unit = { - while (isNumber(in.readChar())) {} + while (isNumber(in.readChar())) () in.retract() } + // FIXME: remove in the next major version def skipString(trace: List[JsonError], in: OneCharReader): Unit = skipString(in, evenBackSlashes = true) @@ -160,7 +161,7 @@ object Lexer { else if (level != 0) skipArray(in, level - 1) } - // useful for embedded documents, e.g. CSV contained inside JSON + // FIXME: remove in the next major version def streamingString(trace: List[JsonError], in: OneCharReader): java.io.Reader = { char(trace, in, '"') new OneCharReader { @@ -357,34 +358,23 @@ object Lexer { case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits BigDecimal", trace) } - // optional whitespace and then an expected character @inline def char(trace: List[JsonError], in: OneCharReader, c: Char): Unit = { val got = in.nextNonWhitespace() if (got != c) error(s"'$c'", got, trace) } - @inline def charOnly( - trace: List[JsonError], - in: OneCharReader, - c: Char - ): Unit = { + @inline def charOnly(trace: List[JsonError], in: OneCharReader, c: Char): Unit = { val got = in.readChar() if (got != c) error(s"'$c'", got, trace) } - // non-positional for performance @inline private[this] def isNumber(c: Char): Boolean = (c: @switch) match { case '+' | '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' | 'e' | 'E' => true case _ => false } - def readChars( - trace: List[JsonError], - in: OneCharReader, - expect: Array[Char], - errMsg: String - ): Unit = { + def readChars(trace: List[JsonError], in: OneCharReader, expect: Array[Char], errMsg: String): Unit = { var i: Int = 0 while (i < expect.length) { if (in.readChar() != expect(i)) error(s"expected '$errMsg'", trace) diff --git a/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala new file mode 100644 index 000000000..155d86754 --- /dev/null +++ b/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala @@ -0,0 +1,18 @@ +package zio.json + +import zio.test.Assertion._ +import zio.test._ + +import scala.collection.immutable + +object CodecVersionSpecificSpec extends ZIOSpecDefault { + val spec: Spec[Environment, Any] = + suite("CodecSpec")( + test("ArraySeq") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = immutable.ArraySeq("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) + } + ) +} diff --git a/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala new file mode 100644 index 000000000..70fb34146 --- /dev/null +++ b/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala @@ -0,0 +1,30 @@ +package zio.json + +import zio.json.ast.Json +import zio.test.Assertion._ +import zio.test._ + +import scala.collection.immutable + +object DecoderVersionSpecificSpec extends ZIOSpecDefault { + + val spec: Spec[Environment, Any] = + suite("Decoder")( + suite("fromJson")( + test("ArraySeq") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = immutable.ArraySeq("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) + } + ), + suite("fromJsonAST")( + test("ArraySeq") { + val json = Json.Arr(Json.Str("5XL"), Json.Str("2XL"), Json.Str("XL")) + val expected = immutable.ArraySeq("5XL", "2XL", "XL") + + assert(json.as[Seq[String]])(isRight(equalTo(expected))) + } + ) + ) +} diff --git a/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala b/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala new file mode 100644 index 000000000..b8c4ac448 --- /dev/null +++ b/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala @@ -0,0 +1,31 @@ +package zio.json + +import zio.json.ast.Json +import zio.test.Assertion._ +import zio.test._ + +import scala.collection.immutable + +object EncoderVesionSpecificSpec extends ZIOSpecDefault { + + val spec: Spec[Environment, Any] = + suite("Encoder")( + suite("toJson")( + test("collections") { + assert(immutable.ArraySeq[Int]().toJson)(equalTo("[]")) && + assert(immutable.ArraySeq(1, 2, 3).toJson)(equalTo("[1,2,3]")) && + assert(immutable.ArraySeq[String]().toJsonPretty)(equalTo("[]")) && + assert(immutable.ArraySeq("foo", "bar").toJsonPretty)(equalTo("[\n \"foo\",\n \"bar\"\n]")) + } + ), + suite("toJsonAST")( + test("collections") { + val arrEmpty = Json.Arr() + val arr123 = Json.Arr(Json.Num(1), Json.Num(2), Json.Num(3)) + + assert(immutable.ArraySeq[Int]().toJsonAST)(isRight(equalTo(arrEmpty))) && + assert(immutable.ArraySeq(1, 2, 3).toJsonAST)(isRight(equalTo(arr123))) + } + ) + ) +} diff --git a/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala new file mode 100644 index 000000000..155d86754 --- /dev/null +++ b/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala @@ -0,0 +1,18 @@ +package zio.json + +import zio.test.Assertion._ +import zio.test._ + +import scala.collection.immutable + +object CodecVersionSpecificSpec extends ZIOSpecDefault { + val spec: Spec[Environment, Any] = + suite("CodecSpec")( + test("ArraySeq") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = immutable.ArraySeq("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) + } + ) +} diff --git a/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala new file mode 100644 index 000000000..70fb34146 --- /dev/null +++ b/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala @@ -0,0 +1,30 @@ +package zio.json + +import zio.json.ast.Json +import zio.test.Assertion._ +import zio.test._ + +import scala.collection.immutable + +object DecoderVersionSpecificSpec extends ZIOSpecDefault { + + val spec: Spec[Environment, Any] = + suite("Decoder")( + suite("fromJson")( + test("ArraySeq") { + val jsonStr = """["5XL","2XL","XL"]""" + val expected = immutable.ArraySeq("5XL", "2XL", "XL") + + assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) + } + ), + suite("fromJsonAST")( + test("ArraySeq") { + val json = Json.Arr(Json.Str("5XL"), Json.Str("2XL"), Json.Str("XL")) + val expected = immutable.ArraySeq("5XL", "2XL", "XL") + + assert(json.as[Seq[String]])(isRight(equalTo(expected))) + } + ) + ) +} diff --git a/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala new file mode 100644 index 000000000..b8c4ac448 --- /dev/null +++ b/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala @@ -0,0 +1,31 @@ +package zio.json + +import zio.json.ast.Json +import zio.test.Assertion._ +import zio.test._ + +import scala.collection.immutable + +object EncoderVesionSpecificSpec extends ZIOSpecDefault { + + val spec: Spec[Environment, Any] = + suite("Encoder")( + suite("toJson")( + test("collections") { + assert(immutable.ArraySeq[Int]().toJson)(equalTo("[]")) && + assert(immutable.ArraySeq(1, 2, 3).toJson)(equalTo("[1,2,3]")) && + assert(immutable.ArraySeq[String]().toJsonPretty)(equalTo("[]")) && + assert(immutable.ArraySeq("foo", "bar").toJsonPretty)(equalTo("[\n \"foo\",\n \"bar\"\n]")) + } + ), + suite("toJsonAST")( + test("collections") { + val arrEmpty = Json.Arr() + val arr123 = Json.Arr(Json.Num(1), Json.Num(2), Json.Num(3)) + + assert(immutable.ArraySeq[Int]().toJsonAST)(isRight(equalTo(arrEmpty))) && + assert(immutable.ArraySeq(1, 2, 3).toJsonAST)(isRight(equalTo(arr123))) + } + ) + ) +} From e56066d16fddb84b2cae1fd24d005ac8575a3d48 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 3 Feb 2025 16:26:42 +0100 Subject: [PATCH 134/311] More efficient decoding of string values (#1280) --- .../shared/src/main/scala/zio/json/internal/lexer.scala | 9 ++++++--- .../src/main/scala/zio/json/internal/writers.scala | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 787220c56..8f05e281d 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -203,7 +203,8 @@ object Lexer { def string(trace: List[JsonError], in: OneCharReader): CharSequence = { var c = in.nextNonWhitespace() if (c != '"') error("'\"'", c, trace) - val sb = new FastStringBuilder(64) + var cs = new Array[Char](64) + var i = 0 while ({ c = in.readChar() c != '"' @@ -222,9 +223,11 @@ object Lexer { case _ => error(c, trace) } } else if (c < ' ') error("invalid control in string", trace) - sb.append(c) + if (i == cs.length) cs = java.util.Arrays.copyOf(cs, i << 1) + cs(i) = c + i += 1 } - sb.buffer + new String(cs, 0, i) } def char(trace: List[JsonError], in: OneCharReader): Char = { diff --git a/zio-json/shared/src/main/scala/zio/json/internal/writers.scala b/zio-json/shared/src/main/scala/zio/json/internal/writers.scala index dff590e2c..997e85530 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/writers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/writers.scala @@ -43,7 +43,7 @@ final class FastStringWrite(initial: Int) extends Write { def buffer: CharSequence = sb } -// like StringBuilder but doesn't have any encoding or range checks +// FIXME: remove in the next major version private[zio] final class FastStringBuilder(initial: Int) { private[this] var chars: Array[Char] = new Array[Char](initial) private[this] var i: Int = 0 From 001cb09170ac39714d5b9c6896e3e04c3c8c8265 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 3 Feb 2025 19:16:26 +0100 Subject: [PATCH 135/311] More efficient decoding of case classes (#1281) --- zio-json/shared/src/main/scala-2.x/zio/json/macros.scala | 1 + zio-json/shared/src/main/scala-3/zio/json/macros.scala | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 9c2dd8a50..05c831cdc 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -8,6 +8,7 @@ import zio.json.internal.{ FieldEncoder, Lexer, RetractReader, StringMatrix, Wri import scala.annotation._ import scala.language.experimental.macros +import scala.reflect.ClassTag /** * If used on a case class field, determines the name of the JSON field. Defaults to the case class field name. diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index dd619e6da..b9aac0d27 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -12,6 +12,7 @@ import zio.json.ast.Json import zio.json.internal.{ FieldEncoder, Lexer, RetractReader, StringMatrix, Write } import scala.annotation._ +import scala.collection.Factory import scala.collection.mutable import scala.language.experimental.macros @@ -516,6 +517,9 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv private final class ArraySeq(p: Array[Any]) extends IndexedSeq[Any] { def apply(i: Int): Any = p(i) def length: Int = p.length + override def to[A](factory: Factory[Any, A]): A = + if (factory.isInstanceOf[Factory[Any, Array[Any]]]) p.asInstanceOf[A] + else super.to(factory) } } @@ -540,6 +544,9 @@ object DeriveJsonDecoder extends JsonDecoderDerivation(JsonCodecConfiguration.de private final class ArraySeq(p: Array[Any]) extends IndexedSeq[Any] { def apply(i: Int): Any = p(i) def length: Int = p.length + override def to[A](factory: Factory[Any, A]): A = + if (factory.isInstanceOf[Factory[Any, Array[Any]]]) p.asInstanceOf[A] + else super.to(factory) } } From d7969a71b59a1b50c1333aef2b010ddb485d4e5d Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 3 Feb 2025 19:49:21 +0100 Subject: [PATCH 136/311] Fix derivation incompatibility after #1239 (#1282) --- .../src/main/scala-2.x/zio/json/macros.scala | 3 +-- .../src/main/scala-3/zio/json/macros.scala | 3 +-- .../src/test/scala/zio/json/DecoderSpec.scala | 16 +++++++++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 05c831cdc..3ca4469ef 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -297,10 +297,9 @@ object DeriveJsonDecoder { @tailrec private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { - case _: OptionJsonDecoder[_] => true case _: CollectionJsonDecoder[_] => !explicitEmptyCollections case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) - case _ => false + case _ => true } override def unsafeDecodeMissing(trace: List[JsonError]): A = { diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index b9aac0d27..1e558faac 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -313,10 +313,9 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv @tailrec private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { - case _: OptionJsonDecoder[_] => true case _: CollectionJsonDecoder[_] => !explicitEmptyCollections case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) - case _ => false + case _ => true } override def unsafeDecodeMissing(trace: List[JsonError]): A = { diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 7263b5252..014105de6 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -504,6 +504,21 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue( json.fromJson[(Foo, Bar)] == Right((Foo(1), Bar("foo"))) ) + }, + test("option custom codec") { + val json = """{"keyStatus": "certified"}""" + final case class Foo(v: String) + final case class RudderSettings(keyStatus: String, policyMode: Option[Foo]) + implicit val encoderOptionPolicyMode: JsonEncoder[Option[Foo]] = JsonEncoder.string.contramap { + case None => "default" + case Some(f) => f.v + } + implicit val decoderOptionPolicyMode: JsonDecoder[Option[Foo]] = JsonDecoder[Option[String]].mapOrFail { + case None | Some("default") => Right(None) + case Some(s) => Right(Some(Foo(s))) + } + implicit lazy val codecRudderSettings: JsonCodec[RudderSettings] = DeriveJsonCodec.gen + assertTrue(json.fromJson[RudderSettings] == Right(RudderSettings("certified", None))) } ), suite("fromJsonAST")( @@ -843,5 +858,4 @@ object DecoderSpec extends ZIOSpecDefault { implicit val eventDecoder: JsonDecoder[Event] = DeriveJsonDecoder.gen[Event] implicit val eventEncoder: JsonEncoder[Event] = DeriveJsonEncoder.gen[Event] } - } From 06f9d72799c8d1fc195505cd6be12e9f1a49275e Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 4 Feb 2025 10:30:26 +0100 Subject: [PATCH 137/311] More efficient encoding of lists (#1284) --- .../src/main/scala/zio/json/JsonEncoder.scala | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index f3b0d09e7..35e0b05a7 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -359,7 +359,54 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { implicit def treeSet[A: JsonEncoder]: JsonEncoder[immutable.TreeSet[A]] = iterable[A, immutable.TreeSet] - implicit def list[A: JsonEncoder]: JsonEncoder[List[A]] = iterable[A, List] + implicit def list[A](implicit A: JsonEncoder[A]): JsonEncoder[List[A]] = + new JsonEncoder[List[A]] { + override def isEmpty(as: List[A]): Boolean = as eq Nil + + def unsafeEncode(as: List[A], indent: Option[Int], out: Write): Unit = + if (as eq Nil) out.write("[]") + else { + out.write('[') + if (indent.isDefined) unsafeEncodePadded(as, indent, out) + else unsafeEncodeCompact(as, indent, out) + out.write(']') + } + + private[this] def unsafeEncodeCompact(as: List[A], indent: Option[Int], out: Write): Unit = { + var as_ = as + var first = true + while (as_ ne Nil) { + if (first) first = false + else out.write(',') + A.unsafeEncode(as_.head, indent, out) + as_ = as_.tail + } + } + + private[this] def unsafeEncodePadded(as: List[A], indent: Option[Int], out: Write): Unit = { + val indent_ = bump(indent) + pad(indent_, out) + var as_ = as + var first = true + while (as_ ne Nil) { + if (first) first = false + else { + out.write(',') + pad(indent_, out) + } + A.unsafeEncode(as_.head, indent_, out) + as_ = as_.tail + } + pad(indent, out) + } + + override final def toJsonAST(as: List[A]): Either[String, Json] = + as.map(A.toJsonAST) + .foldLeft[Either[String, Chunk[Json]]](Right(Chunk.empty)) { (s, i) => + s.flatMap(chunk => i.map(item => chunk :+ item)) + } + .map(Json.Arr(_)) + } implicit def vector[A: JsonEncoder]: JsonEncoder[Vector[A]] = iterable[A, Vector] From 0768ae68c5ee793caa181a66bbeacd92cc729497 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 4 Feb 2025 13:55:54 +0100 Subject: [PATCH 138/311] More efficient decoding of case classes with Scala 3 (#1285) --- build.sbt | 2 +- .../src/main/scala-3/zio/json/macros.scala | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/build.sbt b/build.sbt index 089646a60..ca22ac2ec 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Seq( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.9" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.10" ) case _ => Seq( diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 1e558faac..36998ac72 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -330,7 +330,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } idx += 1 } - ctx.rawConstruct(new ArraySeq(ps)) + ctx.rawConstruct(ps) } override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { @@ -362,7 +362,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } idx += 1 } - ctx.rawConstruct(new ArraySeq(ps)) + ctx.rawConstruct(ps) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = @@ -391,7 +391,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } idx += 1 } - ctx.rawConstruct(new ArraySeq(ps)) + ctx.rawConstruct(ps) case _ => Lexer.error("Not an object", trace) } } @@ -512,13 +512,10 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv inline def gen[A](using mirror: Mirror.Of[A]) = self.derived[A] - // Backcompat for 2.12, otherwise we'd use ArraySeq.unsafeWrapArray + // FIXME: remove in the next major version private final class ArraySeq(p: Array[Any]) extends IndexedSeq[Any] { def apply(i: Int): Any = p(i) def length: Int = p.length - override def to[A](factory: Factory[Any, A]): A = - if (factory.isInstanceOf[Factory[Any, Array[Any]]]) p.asInstanceOf[A] - else super.to(factory) } } @@ -539,13 +536,10 @@ object DeriveJsonDecoder extends JsonDecoderDerivation(JsonCodecConfiguration.de derivation.derived[A] } - // Backcompat for 2.12, otherwise we'd use ArraySeq.unsafeWrapArray + // FIXME: remove in the next major version private final class ArraySeq(p: Array[Any]) extends IndexedSeq[Any] { def apply(i: Int): Any = p(i) def length: Int = p.length - override def to[A](factory: Factory[Any, A]): A = - if (factory.isInstanceOf[Factory[Any, Array[Any]]]) p.asInstanceOf[A] - else super.to(factory) } } From 23c1f85f9e6d44b4029eb0b3b88176e727dc2a20 Mon Sep 17 00:00:00 2001 From: Thijs Broersen <4889512+ThijsBroersen@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:51:53 +0100 Subject: [PATCH 139/311] fix: mapOrFail should use underlying decoder for allowMissingValueDecoder checks (#1283) --- .../src/main/scala-2.x/zio/json/macros.scala | 1 + .../src/main/scala-3/zio/json/macros.scala | 1 + .../src/main/scala/zio/json/JsonDecoder.scala | 8 +- .../json/ConfigurableDeriveCodecSpec.scala | 78 +++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 3ca4469ef..67ad6e98e 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -297,6 +297,7 @@ object DeriveJsonDecoder { @tailrec private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { + case _: OptionJsonDecoder[_] => true case _: CollectionJsonDecoder[_] => !explicitEmptyCollections case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) case _ => true diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 36998ac72..26b33a1af 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -313,6 +313,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv @tailrec private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { + case _: OptionJsonDecoder[_] => true case _: CollectionJsonDecoder[_] => !explicitEmptyCollections case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) case _ => true diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index e6183691d..fba88eb0e 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -161,7 +161,9 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { * with some type of error. */ final def mapOrFail[B](f: A => Either[String, B]): JsonDecoder[B] = - new JsonDecoder[B] { + new MappedJsonDecoder[B] { + private[json] def underlying: JsonDecoder[A] = self + def unsafeDecode(trace: List[JsonError], in: RetractReader): B = f(self.unsafeDecode(trace, in)) match { case Right(b) => b @@ -254,7 +256,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with } def suspend[A](decoder0: => JsonDecoder[A]): JsonDecoder[A] = - new JsonDecoder[A] { + new MappedJsonDecoder[A] { + private[json] def underlying: JsonDecoder[A] = decoder0 + lazy val decoder = decoder0 override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = decoder.unsafeDecode(trace, in) diff --git a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala index 4e8fb105f..a1ba89e22 100644 --- a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala @@ -579,6 +579,48 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen assertTrue("""{}""".fromJson[EmptyListMap].toOption.contains(expectedObj), expectedObj.toJson == expectedStr) + }, + test("for a transform collection") { + case class MappedCollection(a: List[Int]) + case class EmptyMappedCollection(a: MappedCollection) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyMappedCollection(MappedCollection(List.empty)) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[MappedCollection] = JsonCodec + .list[Int] + .transform( + v => MappedCollection(v), + _.a + ) + implicit val emptyMappedCollectionCodec: JsonCodec[EmptyMappedCollection] = DeriveJsonCodec.gen + + assertTrue( + """{}""".fromJson[EmptyMappedCollection].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr + ) + }, + test("for a transformOrFail collection") { + case class MappedCollection(a: List[Int]) + case class EmptyMappedCollection(a: MappedCollection) + val expectedStr = """{"a":[]}""" + val expectedObj = EmptyMappedCollection(MappedCollection(List.empty)) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(decoding = false)) + implicit val codec: JsonCodec[MappedCollection] = JsonCodec + .list[Int] + .transformOrFail( + v => Right(MappedCollection(v)), + _.a + ) + implicit val emptyMappedCollectionCodec: JsonCodec[EmptyMappedCollection] = DeriveJsonCodec.gen + + assertTrue( + """{}""".fromJson[EmptyMappedCollection].toOption.contains(expectedObj), + expectedObj.toJson == expectedStr + ) } ), suite("should not write empty collections and fail missing empty collections")( @@ -768,6 +810,42 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { implicit val codec: JsonCodec[EmptyListMap] = DeriveJsonCodec.gen assertTrue(expectedStr.fromJson[EmptyListMap].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a transform collection") { + case class MappedCollection(a: List[Int]) + case class EmptyMappedCollection(a: MappedCollection) + val expectedStr = """{}""" + val expectedObj = EmptyMappedCollection(MappedCollection(List.empty)) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[MappedCollection] = JsonCodec + .list[Int] + .transform( + v => MappedCollection(v), + _.a + ) + implicit val emptyMappedCollectionCodec: JsonCodec[EmptyMappedCollection] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyMappedCollection].isLeft, expectedObj.toJson == expectedStr) + }, + test("for a transformOrFail collection") { + case class MappedCollection(a: List[Int]) + case class EmptyMappedCollection(a: MappedCollection) + val expectedStr = """{}""" + val expectedObj = EmptyMappedCollection(MappedCollection(List.empty)) + + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(explicitEmptyCollections = ExplicitEmptyCollections(false)) + implicit val codec: JsonCodec[MappedCollection] = JsonCodec + .list[Int] + .transformOrFail( + v => Right(MappedCollection(v)), + _.a + ) + implicit val emptyMappedCollectionCodec: JsonCodec[EmptyMappedCollection] = DeriveJsonCodec.gen + + assertTrue(expectedStr.fromJson[EmptyMappedCollection].isLeft, expectedObj.toJson == expectedStr) } ) ) From 506b8a8a711daa1ba5e677dd07ce0bfd68950099 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:59:36 +0100 Subject: [PATCH 140/311] Update jsoniter-scala-core, ... to 2.33.1 (#1286) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index ca22ac2ec..808741616 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.0" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.1" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.1" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From ef5155d06bdf2621f33e99d1f2f6a2c1f02d60e4 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 5 Feb 2025 10:33:45 +0100 Subject: [PATCH 141/311] More efficient decoding of case classes which have fields with default values (#1287) --- build.sbt | 2 +- zio-json/shared/src/main/scala-2.x/zio/json/macros.scala | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 808741616..84aaadbdb 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Seq( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.10" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.11" ) case _ => Seq( diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 67ad6e98e..a2db48476 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -8,7 +8,6 @@ import zio.json.internal.{ FieldEncoder, Lexer, RetractReader, StringMatrix, Wri import scala.annotation._ import scala.language.experimental.macros -import scala.reflect.ClassTag /** * If used on a case class field, determines the name of the JSON field. Defaults to the case class field name. @@ -659,8 +658,8 @@ object DeriveJsonEncoder { // backcompat for 2.12, otherwise we'd use ArraySeq.unsafeWrapArray private final class ArraySeq(p: Array[Any]) extends IndexedSeq[Any] { - def apply(i: Int): Any = p(i) - def length: Int = p.length + @inline def apply(i: Int): Any = p(i) + @inline def length: Int = p.length } // intercepts the first `{` of a nested writer and discards it. We also need to From ebfcf1d169c0055c598cd62b10f1c2b6128b5380 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 5 Feb 2025 17:22:32 +0100 Subject: [PATCH 142/311] Fix wrong error message when parsing string wuth invalid escaping (#1288) --- .../main/scala/zio/json/internal/lexer.scala | 67 ++++++------------- .../src/test/scala/zio/json/DecoderSpec.scala | 19 ++++++ 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 8f05e281d..aa4988dae 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -85,20 +85,8 @@ object Lexer { c = in.readChar() c != '"' }) { - if (c == '\\') { - (in.readChar(): @switch) match { - case '"' => c = '"' - case '\\' => c = '\\' - case '/' => c = '/' - case 'b' => c = '\b' - case 'f' => c = '\f' - case 'n' => c = '\n' - case 'r' => c = '\r' - case 't' => c = '\t' - case 'u' => c = nextHex4(trace, in) - case _ => error(c, trace) - } - } else if (c < ' ') error("invalid control in string", trace) + if (c == '\\') c = nextEscaped(trace, in) + else if (c < ' ') error("invalid control in string", trace) bs = matrix.update(bs, i, c) i += 1 } @@ -181,7 +169,7 @@ object Lexer { case 'r' => '\r' case 't' => '\t' case 'u' => Lexer.nextHex4(trace, in) - case _ => Lexer.error(c, trace) + case c => Lexer.error(c, trace) }).toInt } else if (c == '\\') { escaped = true @@ -209,20 +197,8 @@ object Lexer { c = in.readChar() c != '"' }) { - if (c == '\\') { - (in.readChar(): @switch) match { - case '"' => c = '"' - case '\\' => c = '\\' - case '/' => c = '/' - case 'b' => c = '\b' - case 'f' => c = '\f' - case 'n' => c = '\n' - case 'r' => c = '\r' - case 't' => c = '\t' - case 'u' => c = nextHex4(trace, in) - case _ => error(c, trace) - } - } else if (c < ' ') error("invalid control in string", trace) + if (c == '\\') c = nextEscaped(trace, in) + else if (c < ' ') error("invalid control in string", trace) if (i == cs.length) cs = java.util.Arrays.copyOf(cs, i << 1) cs(i) = c i += 1 @@ -236,28 +212,29 @@ object Lexer { c = in.readChar() if ( c == '"' || { - if (c == '\\') { - (in.readChar(): @switch) match { - case '"' => c = '"' - case '\\' => c = '\\' - case '/' => c = '/' - case 'b' => c = '\b' - case 'f' => c = '\f' - case 'n' => c = '\n' - case 'r' => c = '\r' - case 't' => c = '\t' - case 'u' => c = nextHex4(trace, in) - case _ => error(c, trace) - } - } else if (c < ' ') error("invalid control in string", trace) + if (c == '\\') c = nextEscaped(trace, in) + else if (c < ' ') error("invalid control in string", trace) in.readChar() != '"' } ) error("expected single character string", trace) c } - // consumes 4 hex characters after current - @noinline def nextHex4(trace: List[JsonError], in: OneCharReader): Char = { + @noinline private[this] def nextEscaped(trace: List[JsonError], in: OneCharReader): Char = + (in.readChar(): @switch) match { + case '"' => '"' + case '\\' => '\\' + case '/' => '/' + case 'b' => '\b' + case 'f' => '\f' + case 'n' => '\n' + case 'r' => '\r' + case 't' => '\t' + case 'u' => nextHex4(trace, in) + case c => error(c, trace) + } + + def nextHex4(trace: List[JsonError], in: OneCharReader): Char = { var i, accum = 0 while (i < 4) { val c = in.readChar() diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 014105de6..84949c6a4 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -16,6 +16,25 @@ object DecoderSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = suite("Decoder")( suite("fromJson")( + test("string") { + assert(""""abc"""".fromJson[String])(isRight(equalTo("abc"))) && + assert(""""abc\n"""".fromJson[String])(isRight(equalTo("abc\n"))) && + assert("\"abc\\u0182\"".fromJson[String])(isRight(equalTo("abcƂ"))) && + assert("\"abc\\u1Ee1\"".fromJson[String])(isRight(equalTo("abcỡ"))) && + assert(""""abc\x"""".fromJson[String])(isLeft(equalTo("""(invalid '\x' in string)"""))) && + assert("\"\u0000\"".fromJson[String])(isLeft(equalTo("""(invalid control in string)"""))) && + assert("\"\\u0000\"".replace('0', 'g').fromJson[String])(isLeft(equalTo("""(invalid charcode in string)"""))) + }, + test("char") { + assert(""""a"""".fromJson[Char])(isRight(equalTo('a'))) && + assert(""""\n"""".fromJson[Char])(isRight(equalTo('\n'))) && + assert("\"\\u0182\"".fromJson[Char])(isRight(equalTo('Ƃ'))) && + assert("\"\\u1Ee1\"".fromJson[Char])(isRight(equalTo('ỡ'))) && + assert(""""aa"""".fromJson[Char])(isLeft(equalTo("""(expected single character string)"""))) && + assert(""""\x"""".fromJson[Char])(isLeft(equalTo("""(invalid '\x' in string)"""))) && + assert("\"\u0000\"".fromJson[Char])(isLeft(equalTo("""(invalid control in string)"""))) && + assert("\"\\u0000\"".replace('0', 'g').fromJson[Char])(isLeft(equalTo("""(invalid charcode in string)"""))) + }, test("byte") { assert("-123".fromJson[Byte])(isRight(equalTo(-123: Byte))) && assert("123".fromJson[Byte])(isRight(equalTo(123: Byte))) && From 22c9a0553fbc16998c7af598134118c00ed65e37 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 6 Feb 2025 08:45:59 +0100 Subject: [PATCH 143/311] Update zio, zio-streams, zio-test, ... to 2.1.15 (#1289) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 84aaadbdb..1caf46a29 100644 --- a/build.sbt +++ b/build.sbt @@ -58,7 +58,7 @@ addCommandAlias( "zioJsonMacrosNative/test" ) -val zioVersion = "2.1.14" +val zioVersion = "2.1.15" lazy val zioJsonRoot = project .in(file(".")) From 049508a139e2e1f231ad21e039d7962764185b0d Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 6 Feb 2025 14:13:31 +0100 Subject: [PATCH 144/311] Encode discriminator field first + clean up error messages (#1290) --- .../src/main/scala-2.x/zio/json/macros.scala | 79 +++--- .../src/main/scala-3/zio/json/macros.scala | 226 +++++++----------- .../src/test/scala/zio/json/CodecSpec.scala | 17 +- .../src/test/scala/zio/json/DecoderSpec.scala | 6 +- 4 files changed, 135 insertions(+), 193 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index a2db48476..1e8cc971a 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -4,7 +4,7 @@ import magnolia1._ import zio.Chunk import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.internal.{ FieldEncoder, Lexer, RetractReader, StringMatrix, Write } +import zio.json.internal.{ FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } import scala.annotation._ import scala.language.experimental.macros @@ -88,10 +88,6 @@ object ziojson_03 { final case class jsonMemberNames(format: JsonMemberFormat) extends Annotation private[json] object jsonMemberNames { - /** - * ~~Stolen~~ Borrowed from jsoniter-scala by Andriy Plokhotnyuk (he even granted permission for this, imagine that!) - */ - import java.lang.Character._ def enforceCamelOrPascalCase(s: String, toPascal: Boolean): String = @@ -231,9 +227,8 @@ object DeriveJsonDecoder { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(_) => ctx.rawConstruct(Nil) - case Json.Null => ctx.rawConstruct(Nil) - case _ => Lexer.error("Not an object", trace) + case _: Json.Obj | Json.Null => ctx.rawConstruct(Nil) + case _ => Lexer.error("expected object", trace) } } else @@ -356,7 +351,6 @@ object DeriveJsonDecoder { } idx += 1 } - ctx.rawConstruct(new ArraySeq(ps)) } @@ -387,7 +381,7 @@ object DeriveJsonDecoder { idx += 1 } ctx.rawConstruct(new ArraySeq(ps)) - case _ => Lexer.error("Not an object", trace) + case _ => Lexer.error("expected object", trace) } } } @@ -412,13 +406,13 @@ object DeriveJsonDecoder { def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - if (discrim.isEmpty) + if (discrim.isEmpty) { + // We're not allowing extra fields in this encoding new JsonDecoder[A] { private[this] val spans = names.map(JsonError.ObjectAccess) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') - // we're not allowing extra fields in this encoding if (Lexer.firstField(trace, in)) { val idx = Lexer.field(trace, in, matrix) if (idx != -1) { @@ -435,22 +429,21 @@ object DeriveJsonDecoder { val keyValue = chunk.head namesMap.get(keyValue._1) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, keyValue._2).asInstanceOf[A] - case _ => Lexer.error("Invalid disambiguator", trace) + case _ => Lexer.error("invalid disambiguator", trace) } - case Json.Obj(_) => Lexer.error("Not an object with a single field", trace) - case _ => Lexer.error("Not an object", trace) + case _ => Lexer.error("expected single field object", trace) } } - else + } else { new JsonDecoder[A] { private[this] val hintfield = discrim.get private[this] val hintmatrix = new StringMatrix(Array(hintfield)) private[this] val spans = names.map(JsonError.Message) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - val in_ = internal.RecordingReader(in) + val in_ = RecordingReader(in) Lexer.char(trace, in_, '{') - if (Lexer.firstField(trace, in_)) + if (Lexer.firstField(trace, in_)) { do { if (Lexer.field(trace, in_, hintmatrix) != -1) { val idx = Lexer.enumeration(trace, in_, matrix) @@ -460,6 +453,7 @@ object DeriveJsonDecoder { } else Lexer.error("invalid disambiguator", trace) } else Lexer.skipValue(trace, in_) } while (Lexer.nextField(trace, in_)) + } Lexer.error(s"missing hint '$hintfield'", trace) } @@ -469,15 +463,15 @@ object DeriveJsonDecoder { fields.find { case (key, _) => key == hintfield } match { case Some((_, Json.Str(name))) => namesMap.get(name) match { - case Some(idx) => tcs(idx).unsafeFromJsonAST(trace, json).asInstanceOf[A] - case _ => Lexer.error("Invalid disambiguator", trace) + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) } - case Some(_) => Lexer.error(s"Non-string hint '$hintfield'", trace) - case _ => Lexer.error(s"Missing hint '$hintfield'", trace) + case _ => Lexer.error(s"missing hint '$hintfield'", trace) } - case _ => Lexer.error("Not an object", trace) + case _ => Lexer.error("expected object", trace) } } + } } def gen[A]: JsonDecoder[A] = macro Magnolia.gen[A] @@ -489,13 +483,11 @@ object DeriveJsonEncoder { def join[A](ctx: CaseClass[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = if (ctx.parameters.isEmpty) new JsonEncoder[A] { - override def isEmpty(a: A): Boolean = true def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = out.write("{}") - override final def toJsonAST(a: A): Either[String, Json] = - Right(Json.Obj(Chunk.empty)) + override final def toJsonAST(a: A): Either[String, Json] = new Right(Json.Obj.empty) } else new JsonEncoder[A] { @@ -557,16 +549,14 @@ object DeriveJsonEncoder { case _ => true } }) { - // if we have at least one field already, we need a comma if (prevFields) { out.write(',') JsonEncoder.pad(indent_, out) - } + } else prevFields = true JsonEncoder.string.unsafeEncode(field.name, indent_, out) if (indent.isEmpty) out.write(':') else out.write(" : ") encoder.unsafeEncode(p, indent_, out) - prevFields = true // record that we have at least one field so far } idx += 1 } @@ -578,7 +568,7 @@ object DeriveJsonEncoder { fields .foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, field) => val param = field.p - val paramValue = field.p.dereference(a).asInstanceOf[param.PType] + val paramValue = param.dereference(a).asInstanceOf[param.PType] field.encodeOrDefault(paramValue)( () => c.flatMap { chunk => @@ -596,8 +586,7 @@ object DeriveJsonEncoder { val names: Array[String] = ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeName.short)) }.toArray - - def discrim = + val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) if (discrim.isEmpty) { @@ -614,16 +603,11 @@ object DeriveJsonEncoder { out.write('}') } - override def toJsonAST(a: A): Either[String, Json] = - ctx.split(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).map { inner => - Json.Obj( - Chunk( - names(sub.index) -> inner - ) - ) - } + override def toJsonAST(a: A): Either[String, Json] = ctx.split(a) { sub => + sub.typeclass.toJsonAST(sub.cast(a)).map { inner => + Json.Obj(Chunk(names(sub.index) -> inner)) } + } } } else { new JsonEncoder[A] { @@ -642,13 +626,14 @@ object DeriveJsonEncoder { sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) } - override def toJsonAST(a: A): Either[String, Json] = - ctx.split(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).flatMap { - case Json.Obj(fields) => Right(Json.Obj(fields :+ hintfield -> Json.Str(names(sub.index)))) - case _ => Left("Subtype is not encoded as an object") - } + override def toJsonAST(a: A): Either[String, Json] = ctx.split(a) { sub => + sub.typeclass.toJsonAST(sub.cast(a)).flatMap { + case Json.Obj(fields) => + new Right(Json.Obj((hintfield -> Json.Str(names(sub.index))) +: fields)) // hint field is always first + case _ => + new Left("expected object") } + } } } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 26b33a1af..5f56b3e2c 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -9,7 +9,7 @@ import zio.Chunk import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.internal.{ FieldEncoder, Lexer, RetractReader, StringMatrix, Write } +import zio.json.internal.{ FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } import scala.annotation._ import scala.collection.Factory @@ -100,11 +100,6 @@ object ziojson_03 { final case class jsonMemberNames(format: JsonMemberFormat) extends Annotation private[json] object jsonMemberNames { - /** - * ~~Stolen~~ Borrowed from jsoniter-scala by Andriy Plokhotnyuk - * (he even granted permission for this, imagine that!) - */ - import java.lang.Character._ def enforceCamelOrPascalCase(s: String, toPascal: Boolean): String = @@ -227,9 +222,8 @@ private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(_) => ctx.rawConstruct(Nil) - case Json.Null => ctx.rawConstruct(Nil) - case _ => Lexer.error("Not an object", trace) + case _: Json.Obj | Json.Null => ctx.rawConstruct(Nil) + case _ => Lexer.error("expected object", trace) } } @@ -290,12 +284,12 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap - private[this] val explicitEmptyCollections = + private val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.decoding }.getOrElse(config.explicitEmptyCollections.decoding) - private[this] val missingValueDecoder = + private val missingValueDecoder = if (explicitEmptyCollections) { lazy val missingValueDecoders = tcs.map { d => if (allowMissingValueDecoder(d)) d @@ -312,7 +306,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } @tailrec - private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { + private def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { case _: OptionJsonDecoder[_] => true case _: CollectionJsonDecoder[_] => !explicitEmptyCollections case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) @@ -333,7 +327,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } ctx.rawConstruct(ps) } - + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') val ps = new Array[Any](len) @@ -393,7 +387,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv idx += 1 } ctx.rawConstruct(ps) - case _ => Lexer.error("Not an object", trace) + case _ => Lexer.error("expected object", trace) } } } @@ -428,22 +422,22 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val idx = Lexer.enumeration(trace, in, matrix) if (idx != -1) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - else Lexer.error("Invalid enumeration value", trace) + else Lexer.error("invalid enumeration value", trace) } override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case Json.Str(typeName) => namesMap.get(typeName) match { case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - case _ => Lexer.error("Invalid enumeration value", trace) + case _ => Lexer.error("invalid enumeration value", trace) } - case _ => Lexer.error("Not a string", trace) + case _ => Lexer.error("expected string", trace) } } } else if (discrim.isEmpty) { // We're not allowing extra fields in this encoding new JsonDecoder[A] { - private val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_)) + private val spans = names.map(JsonError.ObjectAccess(_)) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { Lexer.char(trace, in, '{') @@ -457,18 +451,16 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } else Lexer.error("expected non-empty object", trace) } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = { + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case Json.Obj(chunk) if chunk.size == 1 => val keyValue = chunk.head namesMap.get(keyValue._1) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, keyValue._2).asInstanceOf[A] - case _ => Lexer.error("Invalid disambiguator", trace) + case _ => Lexer.error("invalid disambiguator", trace) } - case Json.Obj(_) => Lexer.error("Not an object with a single field", trace) - case _ => Lexer.error("Not an object", trace) + case _ => Lexer.error("expected single field object", trace) } - } } } else { new JsonDecoder[A] { @@ -477,7 +469,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv private val spans = names.map(JsonError.Message(_)) def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - val in_ = zio.json.internal.RecordingReader(in) + val in_ = RecordingReader(in) Lexer.char(trace, in_, '{') if (Lexer.firstField(trace, in_)) { while ({ @@ -493,20 +485,19 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv Lexer.error(s"missing hint '$hintfield'", trace) } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = { + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case Json.Obj(fields) => fields.find { case (key, _) => key == hintfield } match { - case Some((_, Json.Str(name))) => namesMap.get(name) match { - case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] - case _ => Lexer.error("Invalid disambiguator", trace) - } - case Some(_) => Lexer.error(s"Non-string hint '$hintfield'", trace) - case _ => Lexer.error(s"Missing hint '$hintfield'", trace) + case Some((_, Json.Str(name))) => + namesMap.get(name) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) + } + case _ => Lexer.error(s"missing hint '$hintfield'", trace) } - case _ => Lexer.error("Not an object", trace) + case _ => Lexer.error("expected object", trace) } - } } } } @@ -521,14 +512,11 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } private lazy val caseObjectEncoder = new JsonEncoder[Any] { - override def isEmpty(a: Any): Boolean = true - def unsafeEncode(a: Any, indent: Option[Int], out: Write): Unit = - out.write("{}") + def unsafeEncode(a: Any, indent: Option[Int], out: Write): Unit = out.write("{}") - override final def toJsonAST(a: Any): Either[String, Json] = - Right(Json.Obj(Chunk.empty)) + override final def toJsonAST(a: Any): Either[String, Json] = new Right(Json.Obj.empty) } object DeriveJsonDecoder extends JsonDecoderDerivation(JsonCodecConfiguration.default) { self => @@ -570,7 +558,7 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv a.encoding }.getOrElse(config.explicitEmptyCollections.encoding) - private[this] lazy val fields: Array[FieldEncoder[Any, CaseClass.Param[JsonEncoder, A]]] = params.map { p => + private lazy val fields: Array[FieldEncoder[Any, CaseClass.Param[JsonEncoder, A]]] = params.map { p => val name = p.annotations.collectFirst { case jsonField(name) => name }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) @@ -587,14 +575,14 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv ) } - override def isEmpty(a: A): Boolean = fields.forall { field => - val paramValue = field.p.deref(a) - field.encoder.isEmpty(paramValue) || field.encoder.isNothing(paramValue) - } + override def isEmpty(a: A): Boolean = fields.forall { field => + val paramValue = field.p.deref(a) + field.encoder.isEmpty(paramValue) || field.encoder.isNothing(paramValue) + } def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { out.write('{') - var indent_ = JsonEncoder.bump(indent) + val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) val fields = this.fields var idx = 0 @@ -611,16 +599,14 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv case _ => true } }) { - // if we have at least one field already, we need a comma if (prevFields) { out.write(',') JsonEncoder.pad(indent_, out) - } + } else prevFields = true JsonEncoder.string.unsafeEncode(field.name, indent_, out) if (indent.isEmpty) out.write(':') else out.write(" : ") encoder.unsafeEncode(p, indent_, out) - prevFields = true // at least one field so far } idx += 1 } @@ -628,7 +614,7 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv out.write('}') } - override final def toJsonAST(a: A): Either[String, Json] = { + override final def toJsonAST(a: A): Either[String, Json] = fields .foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, field) => val param = field.p @@ -642,7 +628,6 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv ) } .map(Json.Obj.apply) - } } } @@ -653,109 +638,80 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv ) val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) - val discrim = ctx - .annotations - .collectFirst { - case jsonDiscriminator(n) => n - }.orElse(config.sumTypeHandling.discriminatorField) + val discrim = + ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) if (isEnumeration && discrim.isEmpty) { new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { - val typeName = ctx.choose(a) { sub => - sub - .annotations - .collectFirst { - case jsonHint(name) => name - }.getOrElse(sub.typeInfo.short) - } - - JsonEncoder.string.unsafeEncode(typeName, indent, out) + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.choose(a) { sub => + val name = sub.annotations.collectFirst { + case jsonHint(name) => name + }.getOrElse(jsonHintFormat(sub.typeInfo.short)) + JsonEncoder.string.unsafeEncode(name, indent, out) } - override final def toJsonAST(a: A): Either[String, Json] = { - ctx.choose(a) { sub => - Right( - Json.Str( - sub - .annotations - .collectFirst { - case jsonHint(name) => name - }.getOrElse(sub.typeInfo.short) - ) - ) - } + override final def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => + val name = sub.annotations.collectFirst { + case jsonHint(name) => name + }.getOrElse(jsonHintFormat(sub.typeInfo.short)) + new Right(new Json.Str(name)) } } } else if (discrim.isEmpty) { new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { - ctx.choose(a) { sub => - val name = sub - .annotations - .collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - JsonEncoder.string.unsafeEncode(name, indent_, out) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) - JsonEncoder.pad(indent, out) - out.write('}') - } + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.choose(a) { sub => + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + val name = sub.annotations.collectFirst { + case jsonHint(name) => name + }.getOrElse(jsonHintFormat(sub.typeInfo.short)) + JsonEncoder.string.unsafeEncode(name, indent_, out) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) + JsonEncoder.pad(indent, out) + out.write('}') } - final override def toJsonAST(a: A): Either[String, Json] = { - ctx.choose(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).map { inner => - val name = sub - .annotations - .collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - - Json.Obj( - Chunk( - name -> inner - ) - ) - } + override def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => + sub.typeclass.toJsonAST(sub.cast(a)).map { inner => + val name = sub.annotations.collectFirst { + case jsonHint(name) => name + }.getOrElse(jsonHintFormat(sub.typeInfo.short)) + new Json.Obj(Chunk(name -> inner)) } } } } else { - val hintField = discrim.get - - def getName(annotations: Iterable[_], default: => String): String = - annotations - .collectFirst { case jsonHint(name) => name } - .getOrElse(jsonHintFormat(default)) - new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { - ctx.choose(a) { sub => - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - JsonEncoder.string.unsafeEncode(hintField, indent_, out) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - JsonEncoder.string.unsafeEncode(getName(sub.annotations, sub.typeInfo.short), indent_, out) - // whitespace is always off by 2 spaces at the end, probably not worth fixing - val intermediate = new DeriveJsonEncoder.NestedWriter(out, indent_) - sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) - } + private val hintField = discrim.get + + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.choose(a) { sub => + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + JsonEncoder.string.unsafeEncode(hintField, indent_, out) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + val name = sub.annotations.collectFirst { + case jsonHint(name) => name + }.getOrElse(jsonHintFormat(sub.typeInfo.short)) + JsonEncoder.string.unsafeEncode(name, indent_, out) + // whitespace is always off by 2 spaces at the end, probably not worth fixing + val intermediate = new DeriveJsonEncoder.NestedWriter(out, indent_) + sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) } - override final def toJsonAST(a: A): Either[String, Json] = { - ctx.choose(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).flatMap { - case Json.Obj(fields) => Right(Json.Obj(fields :+ hintField -> Json.Str(getName(sub.annotations, sub.typeInfo.short)))) - case _ => Left("Subtype is not encoded as an object") - } + override final def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => + sub.typeclass.toJsonAST(sub.cast(a)).flatMap { + case Json.Obj(fields) => + val name = sub.annotations.collectFirst { + case jsonHint(name) => name + }.getOrElse(jsonHintFormat(sub.typeInfo.short)) + new Right(new Json.Obj((hintField -> new Json.Str(name)) +: fields)) // hint field is always first + case _ => + new Left("expected object") } } } @@ -772,7 +728,7 @@ object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.de // intercepts the first `{` of a nested writer and discards it. We also need to // inject a `,` unless an empty object `{}` has been written. private[json] final class NestedWriter(out: Write, indent: Option[Int]) extends Write { - private[this] var state = 2 + private var state = 2 def write(c: Char): Unit = if (state != 0) { diff --git a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala index 2d7cbe7dd..60f002c1a 100644 --- a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala @@ -104,13 +104,13 @@ object CodecSpec extends ZIOSpecDefault { }, test("key transformation") { import exampletransformkeys._ - val kebabed = """{"shish123-kebab":""}""" - val snaked = """{"indiana123_jones":""}""" + val kebabed = """{"shi-sh123-kebab":""}""" + val snaked = """{"indi_ana123_jones":""}""" val pascaled = """{"Anders123Hejlsberg":""}""" val cameled = """{"small123Talk":""}""" val overrides = """{"not_modified":"","but-this-should-be":0}""" - val kebabedLegacy = """{"shish-123-kebab":""}""" - val snakedLegacy = """{"indiana_123_jones":""}""" + val kebabedLegacy = """{"shi-sh-123-kebab":""}""" + val snakedLegacy = """{"indi_ana_123_jones":""}""" assert(kebabed.fromJson[Kebabed])(isRight(equalTo(Kebabed("")))) && assert(kebabedLegacy.fromJson[legacy.Kebabed])(isRight(equalTo(legacy.Kebabed("")))) && @@ -249,6 +249,7 @@ object CodecSpec extends ZIOSpecDefault { object Parent { implicit val codec: JsonCodec[Parent] = DeriveJsonCodec.gen[Parent] } + @jsonNoExtraFields case class Child1() extends Parent case class Child2() extends Parent } @@ -307,13 +308,13 @@ object CodecSpec extends ZIOSpecDefault { object exampletransformkeys { @jsonMemberNames(KebabCase) - case class Kebabed(shish123Kebab: String) + case class Kebabed(`shi_sh123Kebab`: String) object Kebabed { implicit val codec: JsonCodec[Kebabed] = DeriveJsonCodec.gen[Kebabed] } @jsonMemberNames(SnakeCase) - case class Snaked(indiana123Jones: String) + case class Snaked(`indi-ana123Jones`: String) object Snaked { implicit val codec: JsonCodec[Snaked] = DeriveJsonCodec.gen[Snaked] } @@ -350,14 +351,14 @@ object CodecSpec extends ZIOSpecDefault { object legacy { @jsonMemberNames(ziojson_03.KebabCase) - case class Kebabed(shish123Kebab: String) + case class Kebabed(shi_sh123Kebab: String) object Kebabed { implicit val codec: JsonCodec[Kebabed] = DeriveJsonCodec.gen[Kebabed] } @jsonMemberNames(ziojson_03.SnakeCase) - case class Snaked(indiana123Jones: String) + case class Snaked(`indi-ana123Jones`: String) object Snaked { implicit val codec: JsonCodec[Snaked] = DeriveJsonCodec.gen[Snaked] diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 84949c6a4..ba61bc278 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -634,15 +634,15 @@ object DecoderSpec extends ZIOSpecDefault { assert(Json.Obj("Child1" -> Json.Obj()).as[Parent])(isRight(equalTo(Child1()))) && assert(Json.Obj("Child2" -> Json.Obj()).as[Parent])(isRight(equalTo(Child2()))) && - assert(Json.Obj("type" -> Json.Str("Child1")).as[Parent])(isLeft(equalTo("(Invalid disambiguator)"))) + assert(Json.Obj("type" -> Json.Str("Child1")).as[Parent])(isLeft(equalTo("(invalid disambiguator)"))) }, test("sum alternative encoding") { import examplealtsum._ assert(Json.Obj("hint" -> Json.Str("Cain")).as[Parent])(isRight(equalTo(Child1()))) && assert(Json.Obj("hint" -> Json.Str("Abel")).as[Parent])(isRight(equalTo(Child2()))) && - assert(Json.Obj("hint" -> Json.Str("Samson")).as[Parent])(isLeft(equalTo("(Invalid disambiguator)"))) && - assert(Json.Obj("Cain" -> Json.Obj()).as[Parent])(isLeft(equalTo("(Missing hint 'hint')"))) + assert(Json.Obj("hint" -> Json.Str("Samson")).as[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && + assert(Json.Obj("Cain" -> Json.Obj()).as[Parent])(isLeft(equalTo("(missing hint 'hint')"))) }, test("Seq") { val json = Json.Arr(Json.Str("5XL"), Json.Str("2XL"), Json.Str("XL")) From 6446a1ce83493866c7307bbcc806a5c23a95b3ed Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 6 Feb 2025 15:56:01 +0100 Subject: [PATCH 145/311] Override Json.Bool.apply (#1291) --- zio-json/shared/src/main/scala/zio/json/ast/ast.scala | 8 ++++++-- .../shared/src/test/scala/zio/json/ast/JsonSpec.scala | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index fca4c9eeb..91707aa96 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -466,8 +466,12 @@ object Json { } object Bool { - val False: Bool = Bool(false) - val True: Bool = Bool(true) + val False: Bool = new Bool(false) + val True: Bool = new Bool(true) + + def apply(value: Boolean): Bool = + if (value) True + else False implicit val decoder: JsonDecoder[Bool] = new JsonDecoder[Bool] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Bool = diff --git a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala index fcfc04e3c..71cfaa7bc 100644 --- a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala @@ -10,6 +10,10 @@ object JsonSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = suite("Json")( suite("apply")( + test("Bool()") { + assertTrue(Json.Bool.True eq Json.Bool(true)) && + assertTrue(Json.Bool.False eq Json.Bool(false)) + }, test("()") { assertTrue(Json.Obj.empty eq Json()) && assertTrue(Json.Obj.empty eq Json.Obj()) && From 84cc7391b5b8b767b0721b1abea03cbb59458daa Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 6 Feb 2025 16:30:48 +0100 Subject: [PATCH 146/311] More efficient encoding to AST (#1292) --- .../src/main/scala/zio/json/JsonEncoder.scala | 26 +++--- .../src/main/scala/zio/json/ast/ast.scala | 85 +++++++++++-------- 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 35e0b05a7..ac0866e3a 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -204,20 +204,18 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def toJsonAST(a: A): Either[String, Json] = encoder.toJsonAST(a) } - implicit val boolean: JsonEncoder[Boolean] = explicit(_.toString, Json.Bool.apply) - implicit val symbol: JsonEncoder[Symbol] = string.contramap(_.name) - implicit val byte: JsonEncoder[Byte] = explicit(_.toString, n => Json.Num(n)) - implicit val short: JsonEncoder[Short] = explicit(_.toString, n => Json.Num(n)) - implicit val int: JsonEncoder[Int] = explicit(_.toString, n => Json.Num(n)) - implicit val long: JsonEncoder[Long] = explicit(_.toString, n => Json.Num(n)) - implicit val bigInteger: JsonEncoder[java.math.BigInteger] = - explicit(_.toString, n => Json.Num(new java.math.BigDecimal(n))) - implicit val scalaBigInt: JsonEncoder[BigInt] = - explicit(_.toString, n => Json.Num(new java.math.BigDecimal(n.bigInteger))) - implicit val double: JsonEncoder[Double] = explicit(SafeNumbers.toString, n => Json.Num(n)) - implicit val float: JsonEncoder[Float] = explicit(SafeNumbers.toString, n => Json.Num(n)) - implicit val bigDecimal: JsonEncoder[java.math.BigDecimal] = explicit(_.toString, Json.Num.apply) - implicit val scalaBigDecimal: JsonEncoder[BigDecimal] = explicit(_.toString, n => Json.Num(n.bigDecimal)) + implicit val boolean: JsonEncoder[Boolean] = explicit(_.toString, Json.Bool.apply) + implicit val symbol: JsonEncoder[Symbol] = string.contramap(_.name) + implicit val byte: JsonEncoder[Byte] = explicit(_.toString, Json.Num.apply) + implicit val short: JsonEncoder[Short] = explicit(_.toString, Json.Num.apply) + implicit val int: JsonEncoder[Int] = explicit(_.toString, Json.Num.apply) + implicit val long: JsonEncoder[Long] = explicit(_.toString, Json.Num.apply) + implicit val bigInteger: JsonEncoder[java.math.BigInteger] = explicit(_.toString, Json.Num.apply) + implicit val scalaBigInt: JsonEncoder[BigInt] = explicit(_.toString, Json.Num.apply) + implicit val double: JsonEncoder[Double] = explicit(SafeNumbers.toString, Json.Num.apply) + implicit val float: JsonEncoder[Float] = explicit(SafeNumbers.toString, Json.Num.apply) + implicit val bigDecimal: JsonEncoder[java.math.BigDecimal] = explicit(_.toString, n => new Json.Num(n)) + implicit val scalaBigDecimal: JsonEncoder[BigDecimal] = explicit(_.toString, Json.Num.apply) implicit def option[A](implicit A: JsonEncoder[A]): JsonEncoder[Option[A]] = new JsonEncoder[Option[A]] { def unsafeEncode(oa: Option[A], indent: Option[Int], out: Write): Unit = diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index 91707aa96..264d1510f 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -369,7 +369,7 @@ object Json { Json.Obj(Chunk.fromArray(array)) } - override def asObject: Some[Json.Obj] = Some(this) + override def asObject: Some[Json.Obj] = new Some(this) override def mapObject(f: Json.Obj => Json.Obj): Json.Obj = f(this) override def mapObjectKeys(f: String => String): Json.Obj = Json.Obj(fields.map(e => f(e._1) -> e._2)) override def mapObjectValues(f: Json => Json): Json.Obj = mapValues(f) @@ -393,8 +393,8 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Obj = json match { - case obj @ Obj(_) => obj - case _ => Lexer.error("Not an object", trace) + case obj: Obj => obj + case _ => Lexer.error("Not an object", trace) } } private lazy val obje = JsonEncoder.keyValueChunk[String, Json] @@ -402,7 +402,7 @@ object Json { def unsafeEncode(a: Obj, indent: Option[Int], out: Write): Unit = obje.unsafeEncode(a.fields, indent, out) - override final def toJsonAST(a: Obj): Either[String, Json] = Right(a) + override final def toJsonAST(a: Obj): Either[String, Json] = new Right(a) } implicit val codec: JsonCodec[Obj] = JsonCodec(encoder, decoder) @@ -424,7 +424,7 @@ object Json { } ++ leftover) } - override def asArray: Some[Chunk[Json]] = Some(elements) + override def asArray: Some[Chunk[Json]] = new Some(elements) override def mapArray(f: Chunk[Json] => Chunk[Json]): Json.Arr = Json.Arr(f(elements)) override def mapArrayValues(f: Json => Json): Json.Arr = Json.Arr(elements.map(f)) } @@ -446,8 +446,8 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Arr = json match { - case arr @ Arr(_) => arr - case _ => Lexer.error("Not an array", trace) + case arr: Arr => arr + case _ => Lexer.error("Not an array", trace) } } private lazy val arre = JsonEncoder.chunk[Json] @@ -455,13 +455,13 @@ object Json { def unsafeEncode(a: Arr, indent: Option[Int], out: Write): Unit = arre.unsafeEncode(a.elements, indent, out) - override final def toJsonAST(a: Arr): Either[String, Json] = Right(a) + override final def toJsonAST(a: Arr): Either[String, Json] = new Right(a) } implicit val codec: JsonCodec[Arr] = JsonCodec(encoder, decoder) } final case class Bool(value: Boolean) extends Json { - override def asBoolean: Some[Boolean] = Some(value) + override def asBoolean: Some[Boolean] = new Some(value) override def mapBoolean(f: Boolean => Boolean): Json.Bool = Json.Bool(f(value)) } @@ -479,22 +479,22 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Bool = json match { - case b @ Bool(_) => b - case _ => Lexer.error("Not a bool value", trace) + case b: Bool => b + case _ => Lexer.error("Not a bool value", trace) } } implicit val encoder: JsonEncoder[Bool] = new JsonEncoder[Bool] { def unsafeEncode(a: Bool, indent: Option[Int], out: Write): Unit = JsonEncoder.boolean.unsafeEncode(a.value, indent, out) - override final def toJsonAST(a: Bool): Either[String, Json] = Right(a) + override final def toJsonAST(a: Bool): Either[String, Json] = new Right(a) } implicit val codec: JsonCodec[Bool] = JsonCodec(encoder, decoder) } final case class Str(value: String) extends Json { - override def asString: Some[String] = Some(value) - override def mapString(f: String => String): Json.Str = Json.Str(f(value)) + override def asString: Some[String] = new Some(value) + override def mapString(f: String => String): Json.Str = new Json.Str(f(value)) } object Str { implicit val decoder: JsonDecoder[Str] = new JsonDecoder[Str] { @@ -503,31 +503,43 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Str = json match { - case s @ Str(_) => s - case _ => Lexer.error("Not a string value", trace) + case s: Str => s + case _ => Lexer.error("Not a string value", trace) } } implicit val encoder: JsonEncoder[Str] = new JsonEncoder[Str] { def unsafeEncode(a: Str, indent: Option[Int], out: Write): Unit = JsonEncoder.string.unsafeEncode(a.value, indent, out) - override final def toJsonAST(a: Str): Either[String, Json] = Right(a) + override final def toJsonAST(a: Str): Either[String, Json] = new Right(a) } implicit val codec: JsonCodec[Str] = JsonCodec(encoder, decoder) } final case class Num(value: java.math.BigDecimal) extends Json { - override def asNumber: Some[Json.Num] = Some(this) - override def mapNumber(f: java.math.BigDecimal => java.math.BigDecimal): Json.Num = Json.Num(f(value)) + override def asNumber: Some[Json.Num] = new Some(this) + override def mapNumber(f: java.math.BigDecimal => java.math.BigDecimal): Json.Num = new Json.Num(f(value)) } object Num { - def apply(value: Byte): Num = Num(BigDecimal(value.toInt).bigDecimal) - def apply(value: Short): Num = Num(BigDecimal(value.toInt).bigDecimal) - def apply(value: Int): Num = Num(BigDecimal(value).bigDecimal) - def apply(value: Long): Num = Num(BigDecimal(value).bigDecimal) - def apply(value: BigDecimal): Num = Num(value.bigDecimal) - def apply(value: Float): Num = Num(BigDecimal.decimal(value).bigDecimal) - def apply(value: Double): Num = Num(BigDecimal(value).bigDecimal) + @inline def apply(value: Byte): Num = apply(value.toInt) + @inline def apply(value: Short): Num = apply(value.toInt) + def apply(value: Int): Num = new Num({ + if (value < 512 && value > -512) new java.math.BigDecimal(value) + else BigDecimal(value).bigDecimal + }) + def apply(value: Long): Num = new Num({ + if (value < 512 && value > -512) new java.math.BigDecimal(value) + else BigDecimal(value).bigDecimal + }) + @inline def apply(value: BigDecimal): Num = new Num(value.bigDecimal) + def apply(value: BigInt): Num = + if (value.isValidLong) apply(value.toLong) + else new Json.Num(new java.math.BigDecimal(value.bigInteger)) + def apply(value: java.math.BigInteger): Num = + if (value.bitCount < 64) apply(value.longValue) + else new Json.Num(new java.math.BigDecimal(value)) + def apply(value: Float): Num = new Num(new java.math.BigDecimal(value.toString)) + def apply(value: Double): Num = new Num(new java.math.BigDecimal(value)) implicit val decoder: JsonDecoder[Num] = new JsonDecoder[Num] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Num = @@ -535,15 +547,15 @@ object Json { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Num = json match { - case n @ Num(_) => n - case _ => Lexer.error("Not a number", trace) + case n: Num => n + case _ => Lexer.error("Not a number", trace) } } implicit val encoder: JsonEncoder[Num] = new JsonEncoder[Num] { def unsafeEncode(a: Num, indent: Option[Int], out: Write): Unit = JsonEncoder.bigDecimal.unsafeEncode(a.value, indent, out) - override final def toJsonAST(a: Num): Either[String, Num] = Right(a) + override final def toJsonAST(a: Num): Either[String, Num] = new Right(a) } implicit val codec: JsonCodec[Num] = JsonCodec(encoder, decoder) @@ -557,22 +569,21 @@ object Json { Null } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Null.type = - json match { - case Null => Null - case _ => Lexer.error("Not null", trace) - } + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Null.type = { + if (json ne Null) Lexer.error("Not null", trace) + Null + } } implicit val encoder: JsonEncoder[Null.type] = new JsonEncoder[Null.type] { def unsafeEncode(a: Null.type, indent: Option[Int], out: Write): Unit = out.write("null") - override final def toJsonAST(a: Null.type): Either[String, Json] = Right(a) + override final def toJsonAST(a: Null.type): Either[String, Json] = new Right(a) } implicit val codec: JsonCodec[Null.type] = JsonCodec(encoder, decoder) - override def asNull: Some[Unit] = Some(()) + override def asNull: Some[Unit] = new Some(()) } implicit val decoder: JsonDecoder[Json] = new JsonDecoder[Json] { @@ -606,7 +617,7 @@ object Json { case Null => Null.encoder.unsafeEncode(Null, indent, out) } - override final def toJsonAST(a: Json): Either[String, Json] = Right(a) + override final def toJsonAST(a: Json): Either[String, Json] = new Right(a) } implicit val codec: JsonCodec[Json] = JsonCodec(encoder, decoder) From f12c952b51390a4a04290363510b2d45bbd3b3e9 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 8 Feb 2025 11:55:46 +0100 Subject: [PATCH 147/311] More efficient encoding of primitives and `java.time._` values (#1293) --- .../zio/json/internal/FastStringWrite.scala | 84 +++++ .../scala/zio/json/internal/SafeNumbers.scala | 286 +++++++++++++--- .../zio/json/internal/FastStringWrite.scala | 171 ++++++++++ .../scala/zio/json/internal/SafeNumbers.scala | 254 +++++++++++--- .../zio/json/internal/FastStringWrite.scala | 171 ++++++++++ .../scala/zio/json/internal/SafeNumbers.scala | 254 +++++++++++--- .../src/main/scala-2.x/zio/json/macros.scala | 72 ++-- .../src/main/scala-3/zio/json/macros.scala | 73 ++-- .../src/main/scala/zio/json/JsonEncoder.scala | 266 ++++++++++++--- .../scala/zio/json/internal/writers.scala | 67 +++- .../scala/zio/json/javatime/serializers.scala | 320 +++++++++++------- 11 files changed, 1658 insertions(+), 360 deletions(-) create mode 100644 zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala create mode 100644 zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala create mode 100644 zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala diff --git a/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala new file mode 100644 index 000000000..c9dc72782 --- /dev/null +++ b/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala @@ -0,0 +1,84 @@ +package zio.json.internal + +final class FastStringWrite(initial: Int) extends Write { + require(initial >= 8) + private[this] var chars: String = "" + + @inline def reset(): Unit = chars = "" + + @inline private[internal] def length: Int = chars.length + + @inline private[internal] def getChars: Array[Char] = chars.toCharArray + + @inline def write(s: String): Unit = chars += s + + @inline def write(c: Char): Unit = chars += c + + @inline override def write(cs: Array[Char], from: Int, to: Int): Unit = { + var i = from + while (i < to) { + chars += cs(i) + i += 1 + } + } + + @inline override def write(c1: Char, c2: Char): Unit = { + chars += c1 + chars += c2 + } + + @inline override def write(c1: Char, c2: Char, c3: Char): Unit = { + chars += c1 + chars += c2 + chars += c3 + } + + @inline override def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = { + chars += c1 + chars += c2 + chars += c3 + chars += c4 + } + + @inline override def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = { + chars += c1 + chars += c2 + chars += c3 + chars += c4 + chars += c5 + } + + @inline override def write(s: Short): Unit = { + chars += (s & 0xff).toChar + chars += (s >> 8).toChar + } + + @inline override def write(s1: Short, s2: Short): Unit = { + chars += (s1 & 0xff).toChar + chars += (s1 >> 8).toChar + chars += (s2 & 0xff).toChar + chars += (s2 >> 8).toChar + } + + @inline override def write(s1: Short, s2: Short, s3: Short): Unit = { + chars += (s1 & 0xff).toChar + chars += (s1 >> 8).toChar + chars += (s2 & 0xff).toChar + chars += (s2 >> 8).toChar + chars += (s3 & 0xff).toChar + chars += (s3 >> 8).toChar + } + + @inline override def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = { + chars += (s1 & 0xff).toChar + chars += (s1 >> 8).toChar + chars += (s2 & 0xff).toChar + chars += (s2 >> 8).toChar + chars += (s3 & 0xff).toChar + chars += (s3 >> 8).toChar + chars += (s4 & 0xff).toChar + chars += (s4 >> 8).toChar + } + + @inline def buffer: CharSequence = chars +} diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 0c9718229..c6545de37 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -72,21 +72,34 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: Double): String = { + val out = new FastStringWrite(24) + write(x, out) + out.toString + } + + def toString(x: Float): String = { + val out = new FastStringWrite(16) + write(x, out) + out.toString + } + // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java - def toString(x: Double): String = { + def write(x: Double, out: Write): Unit = { val bits = java.lang.Double.doubleToLongBits(x) val ieeeExponent = (bits >> 52).toInt & 0x7ff val ieeeMantissa = bits & 0xfffffffffffffL if (ieeeExponent == 2047) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(24) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 1075 var m = ieeeMantissa | 0x10000000000000L @@ -152,40 +165,55 @@ object SafeNumbers { val len = digitCount(dv) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv.toInt | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv.toInt).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv.toInt, out) + out.write('.', '0') + } + } } - s.toString } } - def toString(x: Float): String = { + def write(x: Float, out: Write): Unit = { val bits = java.lang.Float.floatToIntBits(x) val ieeeExponent = (bits >> 23) & 0xff val ieeeMantissa = bits & 0x7fffff if (ieeeExponent == 255) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(16) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 150 var m = ieeeMantissa | 0x800000 @@ -239,30 +267,63 @@ object SafeNumbers { val len = digitCount(dv) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv, out) + out.write('.', '0') + } + } } - s.toString } } - @inline - private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { + private[json] def writeNano(x: Int, out: Write): Unit = { + out.write('.') + var coeff = 100000000 + while (coeff > x) { + out.write('0') + coeff /= 10 + } + write(stripTrailingZeros(x), out) + } + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(24) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w + } + } + + @inline private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { val x = multiplyHigh(g0, cp) + (g1 * cp >>> 1) var y = multiplyHigh(g1, cp) if (x < 0) y += 1 @@ -270,14 +331,12 @@ object SafeNumbers { y } - @inline - private[this] def rop(g: Long, cp: Int): Int = { + @inline private[this] def rop(g: Long, cp: Int): Int = { val x = ((g & 0xffffffffL) * cp >>> 32) + (g >>> 32) * cp (x >>> 31).toInt | -x.toInt >>> 31 } - @inline - private[this] def multiplyHigh(x: Long, y: Long): Long = { + @inline private[this] def multiplyHigh(x: Long, y: Long): Long = { val x2 = x & 0xffffffffL val y2 = y & 0xffffffffL val b = x2 * y2 @@ -287,8 +346,7 @@ object SafeNumbers { (((b >>> 32) + (x1 + x2) * (y1 + y2) - b - a) >>> 32) + a } - @inline - private[this] def stripTrailingZeros(x: Long): Long = { + @inline private[this] def stripTrailingZeros(x: Long): Long = { var q0 = x.toInt if ( q0 == x || { @@ -319,7 +377,7 @@ object SafeNumbers { y } - private[this] def stripTrailingZeros(x: Int): Int = { + @inline private[this] def stripTrailingZeros(x: Int): Int = { var q0 = x var q1 = 0 while ({ @@ -331,6 +389,132 @@ object SafeNumbers { q0 } + @inline def write(a: Long, out: Write): Unit = { + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('9', '2', '2') + q0 = 3372036854775808L + } + } + var q = q0.toInt + if (q0 == q) write(q, out) + else { + var last: Char = 0 + if (q0 >= 1000000000000000000L) { + var z = q0 + q0 = (q0 >>> 1) + (q0 >>> 2) // Based upon the divu10() code from Hacker's Delight 2nd Edition by Henry Warren + q0 += q0 >>> 4 + q0 += q0 >>> 8 + q0 += q0 >>> 16 + q0 += q0 >>> 32 + z -= q0 & 0xfffffffffffffff8L + q0 >>>= 3 + var r = (z - (q0 << 1)).toInt + if (r >= 10) { + q0 += 1L + r -= 10 + } + last = (r | '0').toChar + } + val q1 = ((q0 >>> 8) * 2.56e-6).toLong // divide a medium positive long by 100000000 + q = q1.toInt + if (q1 == q) write(q, out) + else { + q = ((q1 >>> 8) * 1441151881L >>> 49).toInt // divide a small positive long by 100000000 + write(q, out) + write8Digits((q1 - q * 100000000L).toInt, out) + } + write8Digits((q0 - q1 * 100000000L).toInt, out) + if (last != 0) out.write(last) + } + } + + @inline def write(a: Int, out: Write): Unit = { + val ds = digits + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('2') + q0 = 147483648 + } + } + if (q0 < 100) { + if (q0 < 10) out.write((q0 | '0').toChar) + else out.write(ds(q0)) + } else if (q0 < 10000) { + val q1 = q0 * 5243 >> 19 // divide a small positive int by 100 + val d2 = ds(q0 - q1 * 100) + if (q0 < 1000) out.write((q1 | '0').toChar) + else out.write(ds(q1)) + out.write(d2) + } else if (q0 < 1000000) { + val q1 = q0 / 100 + val r1 = q0 - q1 * 100 + val q2 = q1 * 5243 >> 19 // divide a small positive int by 100 + val r2 = q1 - q2 * 100 + if (q0 < 100000) out.write((q2 | '0').toChar) + else out.write(ds(q2)) + out.write(ds(r2), ds(r1)) + } else if (q0 < 100000000) { + if (q0 < 10000000) { + val q1 = q0 / 100 + val r1 = q0 - q1 * 100 + val q2 = q1 / 100 + val r2 = q1 - q2 * 100 + val q3 = q2 * 5243 >> 19 // divide a small positive int by 100 + val r3 = q2 - q3 * 100 + out.write((q3 | '0').toChar) + out.write(ds(r3), ds(r2), ds(r1)) + } else write8Digits(q0, out) + } else { + val q1 = q0 / 100000000 + val r1 = q0 - q1 * 100000000 + if (q0 < 1000000000) out.write((q1 | '0').toChar) + else out.write(ds(q1)) + write8Digits(r1, out) + } + } + + @inline private[this] def write8Digits(x: Int, out: Write): Unit = { + val ds = digits + val q1 = x / 10000 + val q2 = q1 * 5243 >> 19 // divide a small positive int by 100 + out.write(ds(q2), ds(q1 - q2 * 100)) + val r1 = x - q1 * 10000 + val q3 = r1 * 5243 >> 19 // divide a small positive int by 100 + out.write(ds(q3), ds(r1 - q3 * 100)) + } + + @inline private[json] def write4Digits(x: Int, out: Write): Unit = { + val ds = digits + val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 + out.write(ds(q), ds(x - q * 100)) + } + + @inline private[json] def write3Digits(x: Int, out: Write): Unit = { + val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 + out.write((q + '0').toChar) + out.write(digits(x - q * 100)) + } + + @inline private[json] def write2Digits(x: Int, out: Write): Unit = + out.write(digits(x)) + + private[this] final val digits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, + 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, + 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, + 14388, 14644, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 12342, 12598, 12854, 13110, + 13366, 13622, 13878, 14134, 14390, 14646, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 12345, 12601, 12857, 13113, 13369, 13625, + 13881, 14137, 14393, 14649 + ) + @inline private[this] def digitCount(x: Long): Int = if (x >= 1000000000000000L) { diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala new file mode 100644 index 000000000..107d894d6 --- /dev/null +++ b/zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala @@ -0,0 +1,171 @@ +package zio.json.internal + +import java.nio.CharBuffer +import java.util.Arrays + +final class FastStringWrite(initial: Int) extends Write { + require(initial >= 8) + private[this] var chars: Array[Char] = new Array[Char](initial) + private[this] var count: Int = 0 + + @inline def reset(): Unit = count = 0 + + @inline private[internal] def length: Int = count + + @inline private[internal] def getChars: Array[Char] = chars + + def write(s: String): Unit = { + val l = s.length + var cs = chars + val i = count + if (i + l >= cs.length) { + cs = Arrays.copyOf(cs, Math.max(cs.length << 1, i + l)) + chars = cs + } + s.getChars(0, l, cs, i) + count = i + l + } + + def write(c: Char): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c + count = i + 1 + } + + override def write(cs: Array[Char], from: Int, to: Int): Unit = { + var cs_ = chars + val from_ = count + val len = to - from + if (from_ + len >= cs_.length) { + cs_ = Arrays.copyOf(cs_, Math.max(cs_.length << 1, from_ + len)) + chars = cs_ + } + var i = 0 + while (i < len) { + cs_(from_ + i) = cs(from + i) + i += 1 + } + count = from_ + len + } + + override def write(c1: Char, c2: Char): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + count = i + 2 + } + + override def write(c1: Char, c2: Char, c3: Char): Unit = { + var cs = chars + val i = count + if (i + 2 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + count = i + 3 + } + + override def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = { + var cs = chars + val i = count + if (i + 3 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + cs(i + 3) = c4 + count = i + 4 + } + + override def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = { + var cs = chars + val i = count + if (i + 4 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + cs(i + 3) = c4 + cs(i + 4) = c5 + count = i + 5 + } + + override def write(s: Short): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s & 0xff).toChar + cs(i + 1) = (s >> 8).toChar + count = i + 2 + } + + override def write(s1: Short, s2: Short): Unit = { + var cs = chars + val i = count + if (i + 3 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + count = i + 4 + } + + override def write(s1: Short, s2: Short, s3: Short): Unit = { + var cs = chars + val i = count + if (i + 5 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + cs(i + 4) = (s3 & 0xff).toChar + cs(i + 5) = (s3 >> 8).toChar + count = i + 6 + } + + override def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = { + var cs = chars + val i = count + if (i + 7 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + cs(i + 4) = (s3 & 0xff).toChar + cs(i + 5) = (s3 >> 8).toChar + cs(i + 6) = (s4 & 0xff).toChar + cs(i + 7) = (s4 >> 8).toChar + count = i + 8 + } + + def buffer: CharSequence = CharBuffer.wrap(chars, 0, count) +} diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index 4a6489a39..67eec114c 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -72,21 +72,34 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: Double): String = { + val out = new FastStringWrite(24) + write(x, out) + out.toString + } + + def toString(x: Float): String = { + val out = new FastStringWrite(16) + write(x, out) + out.toString + } + // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java - def toString(x: Double): String = { + def write(x: Double, out: Write): Unit = { val bits = java.lang.Double.doubleToLongBits(x) val ieeeExponent = (bits >> 52).toInt & 0x7ff val ieeeMantissa = bits & 0xfffffffffffffL if (ieeeExponent == 2047) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(24) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 1075 var m = ieeeMantissa | 0x10000000000000L @@ -143,40 +156,55 @@ object SafeNumbers { val len = digitCount(dv) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv.toInt | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv.toInt).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv.toInt, out) + out.write('.', '0') + } + } } - s.toString } } - def toString(x: Float): String = { + def write(x: Float, out: Write): Unit = { val bits = java.lang.Float.floatToIntBits(x) val ieeeExponent = (bits >> 23) & 0xff val ieeeMantissa = bits & 0x7fffff if (ieeeExponent == 255) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(16) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 150 var m = ieeeMantissa | 0x800000 @@ -230,25 +258,59 @@ object SafeNumbers { val len = digitCount(dv.toLong) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv, out) + out.write('.', '0') + } + } } - s.toString + } + } + + private[json] def writeNano(x: Int, out: Write): Unit = { + out.write('.') + var coeff = 100000000 + while (coeff > x) { + out.write('0') + coeff = (coeff * 3435973837L >> 35).toInt // divide a positive int by 10 + } + write(stripTrailingZeros(x), out) + } + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(24) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w } } @@ -292,6 +354,114 @@ object SafeNumbers { q0 } + def write(a: Long, out: Write): Unit = { + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('9', '2', '2') + q0 = 3372036854775808L + } + } + val m1 = 100000000L + if (q0 < m1) write(q0.toInt, out) + else { + val m2 = 6189700196426901375L + val q1 = Math.multiplyHigh(q0, m2) >>> 25 // divide a positive long by 100000000 + if (q1 < m1) write(q1.toInt, out) + else { + val q2 = Math.multiplyHigh(q1, m2) >>> 25 // divide a small positive long by 100000000 + write(q2.toInt, out) + write8Digits((q1 - q2 * m1).toInt, out) + } + write8Digits((q0 - q1 * m1).toInt, out) + } + } + + def write(a: Int, out: Write): Unit = { + val ds = digits + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('2') + q0 = 147483648 + } + } + if (q0 < 100) { // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ + if (q0 < 10) out.write((q0 | '0').toChar) + else out.write(ds(q0)) + } else if (q0 < 10000) { + val q1 = q0 * 5243 >> 19 // divide a small positive int by 100 + val d2 = ds(q0 - q1 * 100) + if (q0 < 1000) out.write((q1 | '0').toChar) + else out.write(ds(q1)) + out.write(d2) + } else if (q0 < 1000000) { + val y1 = q0 * 429497L + val y2 = (y1 & 0xffffffffL) * 100 + val y3 = (y2 & 0xffffffffL) * 100 + if (q0 < 100000) out.write(((y1 >> 32).toInt | '0').toChar) + else out.write(ds((y1 >> 32).toInt)) + out.write(ds((y2 >> 32).toInt), ds((y3 >> 32).toInt)) + } else if (q0 < 100000000) { + val y1 = q0 * 140737489L + val y2 = (y1 & 0x7fffffffffffL) * 100 + val y3 = (y2 & 0x7fffffffffffL) * 100 + val y4 = (y3 & 0x7fffffffffffL) * 100 + if (q0 < 10000000) out.write(((y1 >> 47).toInt | '0').toChar) + else out.write(ds((y1 >> 47).toInt)) + out.write(ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) + } else { + val y1 = q0 * 1441151881L + val y2 = (y1 & 0x1ffffffffffffffL) * 100 + val y3 = (y2 & 0x1ffffffffffffffL) * 100 + val y4 = (y3 & 0x1ffffffffffffffL) * 100 + val y5 = (y4 & 0x1ffffffffffffffL) * 100 + if (q0 < 1000000000) out.write(((y1 >>> 57).toInt | '0').toChar) + else out.write(ds((y1 >>> 57).toInt)) + out.write(ds((y2 >>> 57).toInt), ds((y3 >>> 57).toInt), ds((y4 >>> 57).toInt), ds((y5 >>> 57).toInt)) + } + } + + private[this] def write8Digits(x: Int, out: Write): Unit = { + val ds = digits // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ + val y1 = x * 140737489L + val m1 = 0x7fffffffffffL + val m2 = 100L + val y2 = (y1 & m1) * m2 + val y3 = (y2 & m1) * m2 + val y4 = (y3 & m1) * m2 + out.write(ds((y1 >> 47).toInt), ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) + } + + @inline private[json] def write4Digits(x: Int, out: Write): Unit = { + val ds = digits + val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 + out.write(ds(q), ds(x - q * 100)) + } + + @inline private[json] def write3Digits(x: Int, out: Write): Unit = { + val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 + out.write((q + '0').toChar) + out.write(digits(x - q * 100)) + } + + @inline private[json] def write2Digits(x: Int, out: Write): Unit = + out.write(digits(x)) + + private[this] final val digits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, + 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, + 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, + 14388, 14644, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 12342, 12598, 12854, 13110, + 13366, 13622, 13878, 14134, 14390, 14646, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 12345, 12601, 12857, 13113, 13369, 13625, + 13881, 14137, 14393, 14649 + ) + // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt diff --git a/zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala new file mode 100644 index 000000000..107d894d6 --- /dev/null +++ b/zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala @@ -0,0 +1,171 @@ +package zio.json.internal + +import java.nio.CharBuffer +import java.util.Arrays + +final class FastStringWrite(initial: Int) extends Write { + require(initial >= 8) + private[this] var chars: Array[Char] = new Array[Char](initial) + private[this] var count: Int = 0 + + @inline def reset(): Unit = count = 0 + + @inline private[internal] def length: Int = count + + @inline private[internal] def getChars: Array[Char] = chars + + def write(s: String): Unit = { + val l = s.length + var cs = chars + val i = count + if (i + l >= cs.length) { + cs = Arrays.copyOf(cs, Math.max(cs.length << 1, i + l)) + chars = cs + } + s.getChars(0, l, cs, i) + count = i + l + } + + def write(c: Char): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c + count = i + 1 + } + + override def write(cs: Array[Char], from: Int, to: Int): Unit = { + var cs_ = chars + val from_ = count + val len = to - from + if (from_ + len >= cs_.length) { + cs_ = Arrays.copyOf(cs_, Math.max(cs_.length << 1, from_ + len)) + chars = cs_ + } + var i = 0 + while (i < len) { + cs_(from_ + i) = cs(from + i) + i += 1 + } + count = from_ + len + } + + override def write(c1: Char, c2: Char): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + count = i + 2 + } + + override def write(c1: Char, c2: Char, c3: Char): Unit = { + var cs = chars + val i = count + if (i + 2 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + count = i + 3 + } + + override def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = { + var cs = chars + val i = count + if (i + 3 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + cs(i + 3) = c4 + count = i + 4 + } + + override def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = { + var cs = chars + val i = count + if (i + 4 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = c1 + cs(i + 1) = c2 + cs(i + 2) = c3 + cs(i + 3) = c4 + cs(i + 4) = c5 + count = i + 5 + } + + override def write(s: Short): Unit = { + var cs = chars + val i = count + if (i + 1 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s & 0xff).toChar + cs(i + 1) = (s >> 8).toChar + count = i + 2 + } + + override def write(s1: Short, s2: Short): Unit = { + var cs = chars + val i = count + if (i + 3 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + count = i + 4 + } + + override def write(s1: Short, s2: Short, s3: Short): Unit = { + var cs = chars + val i = count + if (i + 5 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + cs(i + 4) = (s3 & 0xff).toChar + cs(i + 5) = (s3 >> 8).toChar + count = i + 6 + } + + override def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = { + var cs = chars + val i = count + if (i + 7 >= cs.length) { + cs = Arrays.copyOf(cs, cs.length << 1) + chars = cs + } + cs(i) = (s1 & 0xff).toChar + cs(i + 1) = (s1 >> 8).toChar + cs(i + 2) = (s2 & 0xff).toChar + cs(i + 3) = (s2 >> 8).toChar + cs(i + 4) = (s3 & 0xff).toChar + cs(i + 5) = (s3 >> 8).toChar + cs(i + 6) = (s4 & 0xff).toChar + cs(i + 7) = (s4 >> 8).toChar + count = i + 8 + } + + def buffer: CharSequence = CharBuffer.wrap(chars, 0, count) +} diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index fcecf03a4..6ca0c36b7 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -72,21 +72,34 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: Double): String = { + val out = new FastStringWrite(24) + write(x, out) + out.toString + } + + def toString(x: Float): String = { + val out = new FastStringWrite(16) + write(x, out) + out.toString + } + // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java - def toString(x: Double): String = { + def write(x: Double, out: Write): Unit = { val bits = java.lang.Double.doubleToLongBits(x) val ieeeExponent = (bits >> 52).toInt & 0x7ff val ieeeMantissa = bits & 0xfffffffffffffL if (ieeeExponent == 2047) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(24) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 1075 var m = ieeeMantissa | 0x10000000000000L @@ -143,40 +156,55 @@ object SafeNumbers { val len = digitCount(dv) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv.toInt | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv.toInt).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv.toInt, out) + out.write('.', '0') + } + } } - s.toString } } - def toString(x: Float): String = { + def write(x: Float, out: Write): Unit = { val bits = java.lang.Float.floatToIntBits(x) val ieeeExponent = (bits >> 23) & 0xff val ieeeMantissa = bits & 0x7fffff if (ieeeExponent == 255) { - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" + out.write( + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + ) } else { - val s = new java.lang.StringBuilder(16) - if (bits < 0) s.append('-') - if (x == 0.0f) s.append('0').append('.').append('0') + if (bits < 0) out.write('-') + if (x == 0.0f) out.write('0', '.', '0') else { var e = ieeeExponent - 150 var m = ieeeMantissa | 0x800000 @@ -230,25 +258,59 @@ object SafeNumbers { val len = digitCount(dv.toLong) exp += len - 1 if (exp < -3 || exp >= 7) { - val dotOff = s.length + 1 - val sdv = stripTrailingZeros(dv) - s.append(sdv) - if (sdv < 10) s.append('0') - s.insert(dotOff, '.').append('E').append(exp) + val sdv = stripTrailingZeros(dv) + if (sdv < 10) out.write((sdv | '0').toChar, '.', '0', 'E') + else { + val w = writes.get + write(sdv, w) + val cs = w.getChars + out.write(cs(0), '.') + out.write(cs, 1, w.length) + out.write('E') + } + write(exp, out) } else if (exp < 0) { - s.append('0').append('.') + out.write('0', '.') while ({ exp += 1 exp != 0 - }) s.append('0') - s.append(stripTrailingZeros(dv)) - } else if (exp + 1 < len) { - val dotOff = s.length + exp + 1 - s.append(stripTrailingZeros(dv)) - s.insert(dotOff, '.') - } else s.append(dv).append('.').append('0') + }) out.write('0') + write(stripTrailingZeros(dv), out) + } else { + exp += 1 + if (exp < len) { + val w = writes.get + write(stripTrailingZeros(dv), w) + val cs = w.getChars + out.write(cs, 0, exp) + out.write('.') + out.write(cs, exp, w.length) + } else { + write(dv, out) + out.write('.', '0') + } + } } - s.toString + } + } + + private[json] def writeNano(x: Int, out: Write): Unit = { + out.write('.') + var coeff = 100000000 + while (coeff > x) { + out.write('0') + coeff = (coeff * 3435973837L >> 35).toInt // divide a positive int by 10 + } + write(stripTrailingZeros(x), out) + } + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(24) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w } } @@ -292,6 +354,114 @@ object SafeNumbers { q0 } + def write(a: Long, out: Write): Unit = { + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('9', '2', '2') + q0 = 3372036854775808L + } + } + val m1 = 100000000L + if (q0 < m1) write(q0.toInt, out) + else { + val m2 = 6189700196426901375L + val q1 = NativeMath.multiplyHigh(q0, m2) >>> 25 // divide a positive long by 100000000 + if (q1 < m1) write(q1.toInt, out) + else { + val q2 = NativeMath.multiplyHigh(q1, m2) >>> 25 // divide a small positive long by 100000000 + write(q2.toInt, out) + write8Digits((q1 - q2 * m1).toInt, out) + } + write8Digits((q0 - q1 * m1).toInt, out) + } + } + + def write(a: Int, out: Write): Unit = { + val ds = digits + var q0 = a + if (q0 < 0) { + q0 = -q0 + out.write('-') + if (q0 == a) { + out.write('2') + q0 = 147483648 + } + } + if (q0 < 100) { // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ + if (q0 < 10) out.write((q0 | '0').toChar) + else out.write(ds(q0)) + } else if (q0 < 10000) { + val q1 = q0 * 5243 >> 19 // divide a small positive int by 100 + val d2 = ds(q0 - q1 * 100) + if (q0 < 1000) out.write((q1 | '0').toChar) + else out.write(ds(q1)) + out.write(d2) + } else if (q0 < 1000000) { + val y1 = q0 * 429497L + val y2 = (y1 & 0xffffffffL) * 100 + val y3 = (y2 & 0xffffffffL) * 100 + if (q0 < 100000) out.write(((y1 >> 32).toInt | '0').toChar) + else out.write(ds((y1 >> 32).toInt)) + out.write(ds((y2 >> 32).toInt), ds((y3 >> 32).toInt)) + } else if (q0 < 100000000) { + val y1 = q0 * 140737489L + val y2 = (y1 & 0x7fffffffffffL) * 100 + val y3 = (y2 & 0x7fffffffffffL) * 100 + val y4 = (y3 & 0x7fffffffffffL) * 100 + if (q0 < 10000000) out.write(((y1 >> 47).toInt | '0').toChar) + else out.write(ds((y1 >> 47).toInt)) + out.write(ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) + } else { + val y1 = q0 * 1441151881L + val y2 = (y1 & 0x1ffffffffffffffL) * 100 + val y3 = (y2 & 0x1ffffffffffffffL) * 100 + val y4 = (y3 & 0x1ffffffffffffffL) * 100 + val y5 = (y4 & 0x1ffffffffffffffL) * 100 + if (q0 < 1000000000) out.write(((y1 >>> 57).toInt | '0').toChar) + else out.write(ds((y1 >>> 57).toInt)) + out.write(ds((y2 >>> 57).toInt), ds((y3 >>> 57).toInt), ds((y4 >>> 57).toInt), ds((y5 >>> 57).toInt)) + } + } + + private[this] def write8Digits(x: Int, out: Write): Unit = { + val ds = digits // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ + val y1 = x * 140737489L + val m1 = 0x7fffffffffffL + val m2 = 100L + val y2 = (y1 & m1) * m2 + val y3 = (y2 & m1) * m2 + val y4 = (y3 & m1) * m2 + out.write(ds((y1 >> 47).toInt), ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) + } + + @inline private[json] def write4Digits(x: Int, out: Write): Unit = { + val ds = digits + val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 + out.write(ds(q), ds(x - q * 100)) + } + + @inline private[json] def write3Digits(x: Int, out: Write): Unit = { + val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 + out.write((q + '0').toChar) + out.write(digits(x - q * 100)) + } + + @inline private[json] def write2Digits(x: Int, out: Write): Unit = + out.write(digits(x)) + + private[this] final val digits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, + 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, + 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, + 14388, 14644, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 12342, 12598, 12854, 13110, + 13366, 13622, 13878, 14134, 14390, 14646, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 12345, 12601, 12857, 13113, 13369, 13625, + 13881, 14137, 14393, 14649 + ) + // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 1e8cc971a..32c4ff86a 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -652,13 +652,15 @@ private final class ArraySeq(p: Array[Any]) extends IndexedSeq[Any] { private[this] final class NestedWriter(out: Write, indent: Option[Int]) extends Write { private[this] var state = 2 - def write(c: Char): Unit = - if (state != 0) { - if (c == ' ' || c == '\n') { - () - } else if (state == 2 && c == '{') { - state = 1 - } else if (state == 1) { + @inline def write(c: Char): Unit = + if (state == 0) out.write(c) + else nonZeroStateWrite(c) + + @noinline private[this] def nonZeroStateWrite(c: Char): Unit = + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { state = 0 if (c != '}') { out.write(',') @@ -666,18 +668,20 @@ private[this] final class NestedWriter(out: Write, indent: Option[Int]) extends } out.write(c) } - } else out.write(c) - - def write(s: String): Unit = - if (state != 0) { - var i = 0 - while (i < s.length) { - val c = s.charAt(i) - if (c == ' ' || c == '\n') { - () - } else if (state == 2 && c == '{') { - state = 1 - } else if (state == 1) { + } + + @inline def write(s: String): Unit = + if (state == 0) out.write(s) + else nonZeroStateWrite(s) + + @noinline private[this] def nonZeroStateWrite(s: String): Unit = { + var i = 0 + while (i < s.length) { + val c = s.charAt(i) + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { state = 0 if (c != '}') { out.write(',') @@ -689,9 +693,35 @@ private[this] final class NestedWriter(out: Write, indent: Option[Int]) extends } return } - i += 1 } - } else out.write(s) + i += 1 + } + } + + @inline override def write(cs: Array[Char], from: Int, to: Int): Unit = + if (state == 0) out.write(cs, from, to) + else nonZeroStateWrite(cs, from, to) + + @noinline def nonZeroStateWrite(cs: Array[Char], from: Int, to: Int): Unit = { + var i = from + while (i < to) { + val c = cs(i) + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { + state = 0 + if (c != '}') { + out.write(',') + JsonEncoder.pad(indent, out) + } + out.write(cs, i, to) + return + } + } + i += 1 + } + } } object DeriveJsonCodec { diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 5f56b3e2c..aadc1c31f 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -730,13 +730,15 @@ object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.de private[json] final class NestedWriter(out: Write, indent: Option[Int]) extends Write { private var state = 2 - def write(c: Char): Unit = - if (state != 0) { - if (c == ' ' || c == '\n') { - () - } else if (state == 2 && c == '{') { - state = 1 - } else if (state == 1) { + @inline def write(c: Char): Unit = + if (state == 0) out.write(c) + else nonZeroStateWrite(c) + + @noinline private def nonZeroStateWrite(c: Char): Unit = { + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { state = 0 if (c != '}') { out.write(',') @@ -744,18 +746,21 @@ object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.de } out.write(c) } - } else out.write(c) - - def write(s: String): Unit = - if (state != 0) { - var i = 0 - while (i < s.length) { - val c = s.charAt(i) - if (c == ' ' || c == '\n') { - () - } else if (state == 2 && c == '{') { - state = 1 - } else if (state == 1) { + } + } + + @inline def write(s: String): Unit = + if (state == 0) out.write(s) + else nonZeroStateWrite(s) + + @noinline private def nonZeroStateWrite(s: String): Unit = { + var i = 0 + while (i < s.length) { + val c = s.charAt(i) + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { state = 0 if (c != '}') { out.write(',') @@ -767,9 +772,35 @@ object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.de } return } - i += 1 } - } else out.write(s) + i += 1 + } + } + + @inline override def write(cs: Array[Char], from: Int, to: Int): Unit = + if (state == 0) out.write(cs, from, to) + else nonZeroStateWrite(cs, from, to) + + @noinline def nonZeroStateWrite(cs: Array[Char], from: Int, to: Int): Unit = { + var i = from + while (i < to) { + val c = cs(i) + if (c != ' ' && c != '\n') { + if (state == 2) { + if (c == '{') state = 1 + } else { + state = 0 + if (c != '}') { + out.write(',') + JsonEncoder.pad(indent, out) + } + out.write(cs, i, to) + return + } + } + i += 1 + } + } } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index ac0866e3a..339bc83d5 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -130,20 +130,20 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: String): Either[String, Json] = new Right(Json.Str(a)) + override final def toJsonAST(a: String): Either[String, Json] = new Right(new Json.Str(a)) private[this] def writeEncoded(a: String, out: Write): Unit = { val len = a.length var i = 0 while (i < len) { (a.charAt(i): @switch) match { - case '"' => out.write("\\\"") - case '\\' => out.write("\\\\") - case '\b' => out.write("\\b") - case '\f' => out.write("\\f") - case '\n' => out.write("\\n") - case '\r' => out.write("\\r") - case '\t' => out.write("\\t") + case '"' => out.write('\\', '"') + case '\\' => out.write('\\', '\\') + case '\b' => out.write('\\', 'b') + case '\f' => out.write('\\', 'f') + case '\n' => out.write('\\', 'n') + case '\r' => out.write('\\', 'r') + case '\t' => out.write('\\', 't') case c => if (c < ' ') out.write("\\u%04x".format(c.toInt)) else out.write(c) @@ -158,13 +158,13 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def unsafeEncode(a: Char, indent: Option[Int], out: Write): Unit = { out.write('"') (a: @switch) match { - case '"' => out.write("\\\"") - case '\\' => out.write("\\\\") - case '\b' => out.write("\\b") - case '\f' => out.write("\\f") - case '\n' => out.write("\\n") - case '\r' => out.write("\\r") - case '\t' => out.write("\\t") + case '"' => out.write('\\', '"') + case '\\' => out.write('\\', '\\') + case '\b' => out.write('\\', 'b') + case '\f' => out.write('\\', 'f') + case '\n' => out.write('\\', 'n') + case '\r' => out.write('\\', 'r') + case '\t' => out.write('\\', 't') case c => if (c < ' ') out.write("\\u%04x".format(c.toInt)) else out.write(c) @@ -172,7 +172,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: Char): Either[String, Json] = new Right(Json.Str(a.toString)) + override final def toJsonAST(a: Char): Either[String, Json] = new Right(new Json.Str(a.toString)) } private[json] def explicit[A](f: A => String, g: A => Json): JsonEncoder[A] = new JsonEncoder[A] { @@ -188,7 +188,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: A): Either[String, Json] = new Right(Json.Str(f(a))) + override final def toJsonAST(a: A): Either[String, Json] = new Right(new Json.Str(f(a))) } def suspend[A](encoder0: => JsonEncoder[A]): JsonEncoder[A] = @@ -204,22 +204,52 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def toJsonAST(a: A): Either[String, Json] = encoder.toJsonAST(a) } - implicit val boolean: JsonEncoder[Boolean] = explicit(_.toString, Json.Bool.apply) - implicit val symbol: JsonEncoder[Symbol] = string.contramap(_.name) - implicit val byte: JsonEncoder[Byte] = explicit(_.toString, Json.Num.apply) - implicit val short: JsonEncoder[Short] = explicit(_.toString, Json.Num.apply) - implicit val int: JsonEncoder[Int] = explicit(_.toString, Json.Num.apply) - implicit val long: JsonEncoder[Long] = explicit(_.toString, Json.Num.apply) + implicit val boolean: JsonEncoder[Boolean] = new JsonEncoder[Boolean] { + def unsafeEncode(a: Boolean, indent: Option[Int], out: Write): Unit = + if (a) out.write('t', 'r', 'u', 'e') + else out.write('f', 'a', 'l', 's', 'e') + + override final def toJsonAST(a: Boolean): Either[String, Json] = new Right(Json.Bool(a)) + } + implicit val symbol: JsonEncoder[Symbol] = string.contramap(_.name) + implicit val byte: JsonEncoder[Byte] = new JsonEncoder[Byte] { + def unsafeEncode(a: Byte, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a.toInt, out) + + override def toJsonAST(a: Byte): Either[String, Json] = new Right(Json.Num(a)) + } + implicit val short: JsonEncoder[Short] = new JsonEncoder[Short] { + def unsafeEncode(a: Short, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a.toInt, out) + + override def toJsonAST(a: Short): Either[String, Json] = new Right(Json.Num(a)) + } + implicit val int: JsonEncoder[Int] = new JsonEncoder[Int] { + def unsafeEncode(a: Int, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: Int): Either[String, Json] = new Right(Json.Num(a)) + } + implicit val long: JsonEncoder[Long] = new JsonEncoder[Long] { + def unsafeEncode(a: Long, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: Long): Either[String, Json] = new Right(Json.Num(a)) + } implicit val bigInteger: JsonEncoder[java.math.BigInteger] = explicit(_.toString, Json.Num.apply) implicit val scalaBigInt: JsonEncoder[BigInt] = explicit(_.toString, Json.Num.apply) - implicit val double: JsonEncoder[Double] = explicit(SafeNumbers.toString, Json.Num.apply) - implicit val float: JsonEncoder[Float] = explicit(SafeNumbers.toString, Json.Num.apply) + implicit val double: JsonEncoder[Double] = new JsonEncoder[Double] { + def unsafeEncode(a: Double, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: Double): Either[String, Json] = new Right(Json.Num(a)) + } + implicit val float: JsonEncoder[Float] = new JsonEncoder[Float] { + def unsafeEncode(a: Float, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: Float): Either[String, Json] = new Right(Json.Num(a)) + } implicit val bigDecimal: JsonEncoder[java.math.BigDecimal] = explicit(_.toString, n => new Json.Num(n)) implicit val scalaBigDecimal: JsonEncoder[BigDecimal] = explicit(_.toString, Json.Num.apply) implicit def option[A](implicit A: JsonEncoder[A]): JsonEncoder[Option[A]] = new JsonEncoder[Option[A]] { def unsafeEncode(oa: Option[A], indent: Option[Int], out: Write): Unit = - if (oa eq None) out.write("null") + if (oa eq None) out.write('n', 'u', 'l', 'l') else A.unsafeEncode(oa.get, indent, out) override def isNothing(oa: Option[A]): Boolean = (oa eq None) || A.isNothing(oa.get) @@ -238,7 +268,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('\n') var i = indent.get while (i > 0) { - out.write(" ") + out.write(' ', ' ') i -= 1 } } @@ -291,6 +321,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with case Right(b) => B.unsafeEncode(b, indent, out) } } + } private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { @@ -301,7 +332,7 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { override def isEmpty(as: Array[A]): Boolean = as.isEmpty def unsafeEncode(as: Array[A], indent: Option[Int], out: Write): Unit = - if (as.isEmpty) out.write("[]") + if (as.isEmpty) out.write('[', ']') else { out.write('[') if (indent.isDefined) unsafeEncodePadded(as, indent, out) @@ -362,7 +393,7 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { override def isEmpty(as: List[A]): Boolean = as eq Nil def unsafeEncode(as: List[A], indent: Option[Int], out: Write): Unit = - if (as eq Nil) out.write("[]") + if (as eq Nil) out.write('[', ']') else { out.write('[') if (indent.isDefined) unsafeEncodePadded(as, indent, out) @@ -438,7 +469,7 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { override def isEmpty(as: T[A]): Boolean = as.isEmpty def unsafeEncode(as: T[A], indent: Option[Int], out: Write): Unit = - if (as.isEmpty) out.write("[]") + if (as.isEmpty) out.write('[', ']') else { out.write('[') if (indent.isDefined) unsafeEncodePadded(as, indent, out) @@ -487,7 +518,7 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { override def isEmpty(a: T[K, A]): Boolean = a.isEmpty def unsafeEncode(kvs: T[K, A], indent: Option[Int], out: Write): Unit = - if (kvs.isEmpty) out.write("{}") + if (kvs.isEmpty) out.write('{', '}') else { out.write('{') if (indent.isDefined) unsafeEncodePadded(kvs, indent, out) @@ -553,22 +584,163 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { import java.time._ - implicit val dayOfWeek: JsonEncoder[DayOfWeek] = stringify(_.toString) - implicit val duration: JsonEncoder[Duration] = stringify(serializers.toString) - implicit val instant: JsonEncoder[Instant] = stringify(serializers.toString) - implicit val localDate: JsonEncoder[LocalDate] = stringify(serializers.toString) - implicit val localDateTime: JsonEncoder[LocalDateTime] = stringify(serializers.toString) - implicit val localTime: JsonEncoder[LocalTime] = stringify(serializers.toString) - implicit val month: JsonEncoder[Month] = stringify(_.toString) - implicit val monthDay: JsonEncoder[MonthDay] = stringify(serializers.toString) - implicit val offsetDateTime: JsonEncoder[OffsetDateTime] = stringify(serializers.toString) - implicit val offsetTime: JsonEncoder[OffsetTime] = stringify(serializers.toString) - implicit val period: JsonEncoder[Period] = stringify(serializers.toString) - implicit val year: JsonEncoder[Year] = stringify(serializers.toString) - implicit val yearMonth: JsonEncoder[YearMonth] = stringify(serializers.toString) - implicit val zonedDateTime: JsonEncoder[ZonedDateTime] = stringify(serializers.toString) - implicit val zoneId: JsonEncoder[ZoneId] = stringify(serializers.toString) - implicit val zoneOffset: JsonEncoder[ZoneOffset] = stringify(serializers.toString) + implicit val dayOfWeek: JsonEncoder[DayOfWeek] = stringify(_.toString) + + implicit val duration: JsonEncoder[Duration] = new JsonEncoder[Duration] { + def unsafeEncode(a: Duration, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: Duration): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val instant: JsonEncoder[Instant] = new JsonEncoder[Instant] { + def unsafeEncode(a: Instant, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: Instant): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val localDate: JsonEncoder[LocalDate] = new JsonEncoder[LocalDate] { + def unsafeEncode(a: LocalDate, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: LocalDate): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val localDateTime: JsonEncoder[LocalDateTime] = new JsonEncoder[LocalDateTime] { + def unsafeEncode(a: LocalDateTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: LocalDateTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val localTime: JsonEncoder[LocalTime] = new JsonEncoder[LocalTime] { + def unsafeEncode(a: LocalTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: LocalTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val month: JsonEncoder[Month] = stringify(_.toString) + + implicit val monthDay: JsonEncoder[MonthDay] = new JsonEncoder[MonthDay] { + def unsafeEncode(a: MonthDay, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: MonthDay): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val offsetDateTime: JsonEncoder[OffsetDateTime] = new JsonEncoder[OffsetDateTime] { + def unsafeEncode(a: OffsetDateTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: OffsetDateTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val offsetTime: JsonEncoder[OffsetTime] = new JsonEncoder[OffsetTime] { + def unsafeEncode(a: OffsetTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: OffsetTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val period: JsonEncoder[Period] = new JsonEncoder[Period] { + def unsafeEncode(a: Period, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: Period): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val year: JsonEncoder[Year] = new JsonEncoder[Year] { + def unsafeEncode(a: Year, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: Year): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val yearMonth: JsonEncoder[YearMonth] = new JsonEncoder[YearMonth] { + def unsafeEncode(a: YearMonth, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: YearMonth): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val zonedDateTime: JsonEncoder[ZonedDateTime] = new JsonEncoder[ZonedDateTime] { + def unsafeEncode(a: ZonedDateTime, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: ZonedDateTime): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val zoneId: JsonEncoder[ZoneId] = new JsonEncoder[ZoneId] { + def unsafeEncode(a: ZoneId, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: ZoneId): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } + + implicit val zoneOffset: JsonEncoder[ZoneOffset] = new JsonEncoder[ZoneOffset] { + def unsafeEncode(a: ZoneOffset, indent: Option[Int], out: Write): Unit = { + out.write('"') + serializers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: ZoneOffset): Either[String, Json] = + new Right(new Json.Str(serializers.toString(a))) + } implicit val uuid: JsonEncoder[UUID] = stringify(_.toString) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/writers.scala b/zio-json/shared/src/main/scala/zio/json/internal/writers.scala index 997e85530..b4130ba08 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/writers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/writers.scala @@ -25,6 +25,63 @@ import java.util.Arrays trait Write { def write(c: Char): Unit def write(s: String): Unit + def write(cs: Array[Char], from: Int, to: Int): Unit = { + var i = from + while (i < to) { + write(cs(i)) + i += 1 + } + } + @inline def write(c1: Char, c2: Char): Unit = { + write(c1) + write(c2) + } + @inline def write(c1: Char, c2: Char, c3: Char): Unit = { + write(c1) + write(c2) + write(c3) + } + @inline def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = { + write(c1) + write(c2) + write(c3) + write(c4) + } + @inline def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = { + write(c1) + write(c2) + write(c3) + write(c4) + write(c5) + } + @inline def write(s: Short): Unit = { + write((s & 0xff).toChar) + write((s >> 8).toChar) + } + @inline def write(s1: Short, s2: Short): Unit = { + write((s1 & 0xff).toChar) + write((s1 >> 8).toChar) + write((s2 & 0xff).toChar) + write((s2 >> 8).toChar) + } + @inline def write(s1: Short, s2: Short, s3: Short): Unit = { + write((s1 & 0xff).toChar) + write((s1 >> 8).toChar) + write((s2 & 0xff).toChar) + write((s2 >> 8).toChar) + write((s3 & 0xff).toChar) + write((s3 >> 8).toChar) + } + @inline def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = { + write((s1 & 0xff).toChar) + write((s1 >> 8).toChar) + write((s2 & 0xff).toChar) + write((s2 >> 8).toChar) + write((s3 & 0xff).toChar) + write((s3 >> 8).toChar) + write((s4 & 0xff).toChar) + write((s4 >> 8).toChar) + } } // wrapper to implement the legacy Java API @@ -33,16 +90,6 @@ final class WriteWriter(out: java.io.Writer) extends Write { def write(c: Char): Unit = out.write(c.toInt) } -final class FastStringWrite(initial: Int) extends Write { - private[this] val sb: java.lang.StringBuilder = new java.lang.StringBuilder(initial) - - def write(s: String): Unit = sb.append(s): Unit - - def write(c: Char): Unit = sb.append(c): Unit - - def buffer: CharSequence = sb -} - // FIXME: remove in the next major version private[zio] final class FastStringBuilder(initial: Int) { private[this] var chars: Array[Char] = new Array[Char](initial) diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala index 4f58364c6..74ac25c51 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala @@ -15,15 +15,22 @@ */ package zio.json.javatime +import zio.json.internal.{ FastStringWrite, SafeNumbers, Write } + import java.time._ private[json] object serializers { def toString(x: Duration): String = { - val s = new java.lang.StringBuilder(16) - s.append('P').append('T') + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: Duration, out: Write): Unit = { + out.write('P', 'T') val totalSecs = x.getSeconds var nano = x.getNano - if ((totalSecs | nano) == 0) s.append('0').append('S') + if ((totalSecs | nano) == 0) out.write('0', 'S') else { var effectiveTotalSecs = totalSecs if (totalSecs < 0 && nano > 0) effectiveTotalSecs += 1 @@ -31,28 +38,33 @@ private[json] object serializers { val secsOfHour = (effectiveTotalSecs - hours * 3600).toInt val minutes = secsOfHour / 60 val seconds = secsOfHour - minutes * 60 - if (hours != 0) s.append(hours).append('H') - if (minutes != 0) s.append(minutes).append('M') + if (hours != 0) { + SafeNumbers.write(hours, out) + out.write('H') + } + if (minutes != 0) { + SafeNumbers.write(minutes, out) + out.write('M') + } if ((seconds | nano) != 0) { - if (totalSecs < 0 && seconds == 0) s.append('-').append('0') - else s.append(seconds) + if (totalSecs < 0 && seconds == 0) out.write('-', '0') + else SafeNumbers.write(seconds, out) if (nano != 0) { if (totalSecs < 0) nano = 1000000000 - nano - val dotPos = s.length - s.append(nano + 1000000000) - var i = s.length - 1 - while (s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s.setCharAt(dotPos, '.') + SafeNumbers.writeNano(nano, out) } - s.append('S') + out.write('S') } } - s.toString } def toString(x: Instant): String = { - val s = new java.lang.StringBuilder(32) + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: Instant, out: Write): Unit = { val epochSecond = x.getEpochSecond val epochDay = (if (epochSecond >= 0) epochSecond @@ -82,187 +94,243 @@ private[json] object serializers { val secsOfHour = secsOfDay - hour * 3600 val minute = secsOfHour * 17477 >> 20 // divide a small positive int by 60 val second = secsOfHour - minute * 60 - appendYear(year, s) - append2Digits(month, s.append('-')) - append2Digits(day, s.append('-')) - append2Digits(hour, s.append('T')) - append2Digits(minute, s.append(':')) - append2Digits(second, s.append(':')) + writeYear(year, out) + out.write('-') + SafeNumbers.write2Digits(month, out) + out.write('-') + SafeNumbers.write2Digits(day, out) + out.write('T') + SafeNumbers.write2Digits(hour, out) + out.write(':') + SafeNumbers.write2Digits(minute, out) + out.write(':') + SafeNumbers.write2Digits(second, out) val nano = x.getNano if (nano != 0) { - s.append('.') + out.write('.') val q1 = nano / 1000000 val r1 = nano - q1 * 1000000 - append3Digits(q1, s) + SafeNumbers.write3Digits(q1, out) if (r1 != 0) { val q2 = r1 / 1000 val r2 = r1 - q2 * 1000 - append3Digits(q2, s) - if (r2 != 0) append3Digits(r2, s) + SafeNumbers.write3Digits(q2, out) + if (r2 != 0) SafeNumbers.write3Digits(r2, out) } } - s.append('Z').toString + out.write('Z') } def toString(x: LocalDate): String = { - val s = new java.lang.StringBuilder(16) - appendLocalDate(x, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: LocalDate, out: Write): Unit = { + writeYear(x.getYear, out) + out.write('-') + SafeNumbers.write2Digits(x.getMonthValue, out) + out.write('-') + SafeNumbers.write2Digits(x.getDayOfMonth, out) } def toString(x: LocalDateTime): String = { - val s = new java.lang.StringBuilder(32) - appendLocalDate(x.toLocalDate, s) - appendLocalTime(x.toLocalTime, s.append('T')) - s.toString + val out = writes.get + write(x, out) + write(x.toLocalDate, out) + out.buffer.toString + } + + def write(x: LocalDateTime, out: Write): Unit = { + write(x.toLocalDate, out) + out.write('T') + write(x.toLocalTime, out) } def toString(x: LocalTime): String = { - val s = new java.lang.StringBuilder(24) - appendLocalTime(x, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: LocalTime, out: Write): Unit = { + SafeNumbers.write2Digits(x.getHour, out) + out.write(':') + SafeNumbers.write2Digits(x.getMinute, out) + out.write(':') + SafeNumbers.write2Digits(x.getSecond, out) + val nano = x.getNano + if (nano != 0) SafeNumbers.writeNano(nano, out) } def toString(x: MonthDay): String = { - val s = new java.lang.StringBuilder(8) - append2Digits(x.getMonthValue, s.append('-').append('-')) - append2Digits(x.getDayOfMonth, s.append('-')) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: MonthDay, out: Write): Unit = { + out.write('-', '-') + SafeNumbers.write2Digits(x.getMonthValue, out) + out.write('-') + SafeNumbers.write2Digits(x.getDayOfMonth, out) } def toString(x: OffsetDateTime): String = { - val s = new java.lang.StringBuilder(48) - appendLocalDate(x.toLocalDate, s) - appendLocalTime(x.toLocalTime, s.append('T')) - appendZoneOffset(x.getOffset, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: OffsetDateTime, out: Write): Unit = { + write(x.toLocalDate, out) + out.write('T') + write(x.toLocalTime, out) + write(x.getOffset, out) } def toString(x: OffsetTime): String = { - val s = new java.lang.StringBuilder(32) - appendLocalTime(x.toLocalTime, s) - appendZoneOffset(x.getOffset, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: OffsetTime, out: Write): Unit = { + write(x.toLocalTime, out) + write(x.getOffset, out) } def toString(x: Period): String = { - val s = new java.lang.StringBuilder(16) - s.append('P') - if (x.isZero) s.append('0').append('D') + val out = writes.get + write(x, out) + out.buffer.toString + } + + def write(x: Period, out: Write): Unit = { + out.write('P') + if (x.isZero) out.write('0', 'D') else { val years = x.getYears val months = x.getMonths val days = x.getDays - if (years != 0) s.append(years).append('Y') - if (months != 0) s.append(months).append('M') - if (days != 0) s.append(days).append('D') + if (years != 0) { + SafeNumbers.write(years, out) + out.write('Y') + } + if (months != 0) { + SafeNumbers.write(months, out) + out.write('M') + } + if (days != 0) { + SafeNumbers.write(days, out) + out.write('D') + } } - s.toString } def toString(x: Year): String = { - val s = new java.lang.StringBuilder(16) - appendYear(x.getValue, s) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString } + @inline def write(x: Year, out: Write): Unit = writeYear(x.getValue, out) + def toString(x: YearMonth): String = { - val s = new java.lang.StringBuilder(16) - appendYear(x.getYear, s) - append2Digits(x.getMonthValue, s.append('-')) - s.toString + val out = writes.get + write(x, out) + out.buffer.toString } - def toString(x: ZonedDateTime): String = { - val s = new java.lang.StringBuilder(48) - appendLocalDate(x.toLocalDate, s) - appendLocalTime(x.toLocalTime, s.append('T')) - appendZoneOffset(x.getOffset, s) - val zone = x.getZone - if (!zone.isInstanceOf[ZoneOffset]) s.append('[').append(zone.getId).append(']') - s.toString + def write(x: YearMonth, out: Write): Unit = { + writeYear(x.getYear, out) + out.write('-') + SafeNumbers.write2Digits(x.getMonthValue, out) } - def toString(x: ZoneId): String = x.getId - - def toString(x: ZoneOffset): String = { - val s = new java.lang.StringBuilder(16) - appendZoneOffset(x, s) - s.toString + def toString(x: ZonedDateTime): String = { + val out = writes.get + write(x, out) + out.buffer.toString } - private[this] def appendLocalDate(x: LocalDate, s: java.lang.StringBuilder): Unit = { - appendYear(x.getYear, s) - append2Digits(x.getMonthValue, s.append('-')) - append2Digits(x.getDayOfMonth, s.append('-')) + def write(x: ZonedDateTime, out: Write): Unit = { + write(x.toLocalDate, out) + out.write('T') + write(x.toLocalTime, out) + write(x.getOffset, out) + val zone = x.getZone + if (!zone.isInstanceOf[ZoneOffset]) { + out.write('[') + out.write(zone.getId) + out.write(']') + } } - private[this] def appendLocalTime(x: LocalTime, s: java.lang.StringBuilder): Unit = { - append2Digits(x.getHour, s) - append2Digits(x.getMinute, s.append(':')) - append2Digits(x.getSecond, s.append(':')) - val nano = x.getNano - if (nano != 0) { - val dotPos = s.length - s.append(nano + 1000000000) - var i = s.length - 1 - while (s.charAt(i) == '0') i -= 1 - s.setLength(i + 1) - s.setCharAt(dotPos, '.') - } + @inline def toString(x: ZoneId): String = x.getId + + @inline def write(x: ZoneId, out: Write): Unit = out.write(x.getId) + + def toString(x: ZoneOffset): String = { + val out = writes.get + write(x, out) + out.buffer.toString } - private[this] def appendZoneOffset(x: ZoneOffset, s: java.lang.StringBuilder): Unit = { + def write(x: ZoneOffset, out: Write): Unit = { val totalSeconds = x.getTotalSeconds - if (totalSeconds == 0) s.append('Z'): Unit + if (totalSeconds == 0) out.write('Z'): Unit else { val q0 = if (totalSeconds > 0) { - s.append('+') + out.write('+') totalSeconds } else { - s.append('-') + out.write('-') -totalSeconds } val q1 = q0 * 37283 >>> 27 // divide a small positive int by 3600 val r1 = q0 - q1 * 3600 - append2Digits(q1, s) - s.append(':') + SafeNumbers.write2Digits(q1, out) + out.write(':') val q2 = r1 * 17477 >> 20 // divide a small positive int by 60 val r2 = r1 - q2 * 60 - append2Digits(q2, s) - if (r2 != 0) append2Digits(r2, s.append(':')) + SafeNumbers.write2Digits(q2, out) + if (r2 != 0) { + out.write(':') + SafeNumbers.write2Digits(r2, out) + } } } - private[this] def appendYear(x: Int, s: java.lang.StringBuilder): Unit = + private[this] def writeYear(x: Int, out: Write): Unit = if (x >= 0) { - if (x < 10000) append4Digits(x, s) - else s.append('+').append(x): Unit - } else if (x > -10000) append4Digits(-x, s.append('-')) - else s.append(x): Unit - - private[this] def append4Digits(x: Int, s: java.lang.StringBuilder): Unit = { - val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 - append2Digits(q, s) - append2Digits(x - q * 100, s) - } - - private[this] def append3Digits(x: Int, s: java.lang.StringBuilder): Unit = { - val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 - append2Digits(x - q * 100, s.append((q + '0').toChar)) - } - - private[this] def append2Digits(x: Int, s: java.lang.StringBuilder): Unit = { - val q = x * 103 >> 10 // divide a 2-digit positive int by 10 - s.append((q + '0').toChar).append((x + '0' - q * 10).toChar): Unit - } + if (x < 10000) SafeNumbers.write4Digits(x, out) + else { + out.write('+') + SafeNumbers.write(x, out): Unit + } + } else if (x > -10000) { + out.write('-') + SafeNumbers.write4Digits(-x, out) + } else SafeNumbers.write(x, out): Unit - private[this] def to400YearCycle(day: Long): Int = + @inline private[this] def to400YearCycle(day: Long): Int = (day / 146097).toInt // 146097 == number of days in a 400 year cycle - private[this] def toMarchDayOfYear(marchZeroDay: Long, year: Int): Int = { + @inline private[this] def toMarchDayOfYear(marchZeroDay: Long, year: Int): Int = { val century = year / 100 (marchZeroDay - year * 365L).toInt - (year >> 2) + century - (century >> 2) } + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(64) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w + } + } } From 2d6cc6ffba0cae1661411c7a5d1cc5bfd1d9d634 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 8 Feb 2025 22:54:27 +0100 Subject: [PATCH 148/311] Fix unexpected runtime exceptions with Scala Native on Windows (#1294) --- .../scala/zio/json/internal/SafeNumbers.scala | 16 ++++++++-------- .../scala/zio/json/internal/UnsafeNumbers.scala | 9 ++++++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index 6ca0c36b7..67eec114c 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -134,7 +134,7 @@ object SafeNumbers { val s = vb >> 2 if ( s < 100 || { - dv = NativeMath.multiplyHigh(s, 1844674407370955168L) // divide a positive long by 10 + dv = Math.multiplyHigh(s, 1844674407370955168L) // divide a positive long by 10 val sp40 = dv * 40 val upin = (vbls - sp40).toInt (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { @@ -315,12 +315,12 @@ object SafeNumbers { } private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { - val x = NativeMath.multiplyHigh(g0, cp) + (g1 * cp >>> 1) - NativeMath.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63 + val x = Math.multiplyHigh(g0, cp) + (g1 * cp >>> 1) + Math.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63 } private[this] def rop(g: Long, cp: Int): Int = { - val x = NativeMath.multiplyHigh(g, cp.toLong << 32) + val x = Math.multiplyHigh(g, cp.toLong << 32) (x >>> 31).toInt | -x.toInt >>> 31 } @@ -328,13 +328,13 @@ object SafeNumbers { var q0 = x.toInt if ( q0 == x || { - q0 = (NativeMath.multiplyHigh(x, 6189700196426901375L) >>> 25).toInt // divide a positive long by 100000000 + q0 = (Math.multiplyHigh(x, 6189700196426901375L) >>> 25).toInt // divide a positive long by 100000000 (x - q0 * 100000000L).toInt == 0 } ) return stripTrailingZeros(q0).toLong var y, q1 = x while ({ - q1 = NativeMath.multiplyHigh(q1, 1844674407370955168L) // divide a positive long by 10 + q1 = Math.multiplyHigh(q1, 1844674407370955168L) // divide a positive long by 10 q1 * 10 == y }) y = q1 y @@ -368,10 +368,10 @@ object SafeNumbers { if (q0 < m1) write(q0.toInt, out) else { val m2 = 6189700196426901375L - val q1 = NativeMath.multiplyHigh(q0, m2) >>> 25 // divide a positive long by 100000000 + val q1 = Math.multiplyHigh(q0, m2) >>> 25 // divide a positive long by 100000000 if (q1 < m1) write(q1.toInt, out) else { - val q2 = NativeMath.multiplyHigh(q1, m2) >>> 25 // divide a small positive long by 100000000 + val q2 = Math.multiplyHigh(q1, m2) >>> 25 // divide a small positive long by 100000000 write(q2.toInt, out) write8Digits((q1 - q2 * m1).toInt, out) } diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 3b9b9f2df..a41908157 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -344,12 +344,12 @@ object UnsafeNumbers { // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct - @noinline private[this] def toFloat(m10: Long, e10: Int): Float = + private[this] def toFloat(m10: Long, e10: Int): Float = if (m10 == 0 || e10 < -64) 0.0f else if (e10 >= 39) Float.PositiveInfinity else { var shift = java.lang.Long.numberOfLeadingZeros(m10) - var m2 = NativeMath.unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 shift = java.lang.Long.numberOfLeadingZeros(m2) m2 <<= shift @@ -481,7 +481,7 @@ object UnsafeNumbers { else if (e10 >= 310) Double.PositiveInfinity else { var shift = java.lang.Long.numberOfLeadingZeros(m10) - var m2 = NativeMath.unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) + var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 shift = java.lang.Long.numberOfLeadingZeros(m2) m2 <<= shift @@ -518,6 +518,9 @@ object UnsafeNumbers { if (consume && current != -1) throw UnsafeNumber } + @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = + Math.multiplyHigh(x, y) + x + y // FIXME: Use Math.unsignedMultiplyHigh after dropping of JDK 17 support + private[this] final val pow10Doubles: Array[Double] = Array(1, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22) From addd7ff7f9aae7ebbf68fd463e4b2401ae1ae69f Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 9 Feb 2025 09:47:43 +0100 Subject: [PATCH 149/311] More efficient encoding of UUIDs (#1295) --- .../scala/zio/json/internal/SafeNumbers.scala | 51 ++++++++++++++++++- .../scala/zio/json/internal/SafeNumbers.scala | 51 ++++++++++++++++++- .../scala/zio/json/internal/SafeNumbers.scala | 51 ++++++++++++++++++- .../src/main/scala/zio/json/JsonEncoder.scala | 15 ++++-- 4 files changed, 159 insertions(+), 9 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index c6545de37..d37b1369e 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -15,6 +15,8 @@ */ package zio.json.internal +import java.util.UUID + /** * Total, fast, number parsing. * @@ -75,13 +77,19 @@ object SafeNumbers { def toString(x: Double): String = { val out = new FastStringWrite(24) write(x, out) - out.toString + out.buffer.toString } def toString(x: Float): String = { val out = new FastStringWrite(16) write(x, out) - out.toString + out.buffer.toString + } + + def toString(x: UUID): String = { + val out = writes.get + write(x, out) + out.buffer.toString } // Based on the amazing work of Raffaello Giulietti @@ -303,6 +311,26 @@ object SafeNumbers { } } + def write(x: UUID, out: Write): Unit = { + val ds = lowerCaseHexDigits + val msb = x.getMostSignificantBits + val lsb = x.getLeastSignificantBits + val msb1 = (msb >> 32).toInt + out.write(ds(msb1 >>> 24), ds(msb1 >> 16 & 0xff), ds(msb1 >> 8 & 0xff), ds(msb1 & 0xff)) + out.write('-') + val msb2 = msb.toInt + out.write(ds(msb2 >>> 24), ds(msb2 >> 16 & 0xff)) + out.write('-') + out.write(ds(msb2 >> 8 & 0xff), ds(msb2 & 0xff)) + out.write('-') + val lsb1 = (lsb >>> 32).toInt + out.write(ds(lsb1 >>> 24), ds(lsb1 >> 16 & 0xff)) + out.write('-') + out.write(ds(lsb1 >> 8 & 0xff), ds(lsb1 & 0xff)) + val lsb2 = lsb.toInt + out.write(ds(lsb2 >>> 24), ds(lsb2 >> 16 & 0xff), ds(lsb2 >> 8 & 0xff), ds(lsb2 & 0xff)) + } + private[json] def writeNano(x: Int, out: Write): Unit = { out.write('.') var coeff = 100000000 @@ -549,6 +577,25 @@ object SafeNumbers { else 10 } + private final val lowerCaseHexDigits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 24880, 25136, 25392, 25648, 25904, 26160, + 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 24881, 25137, 25393, 25649, 25905, 26161, + 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 24882, 25138, 25394, 25650, 25906, 26162, + 12339, 12595, 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 24883, 25139, 25395, 25651, 25907, 26163, + 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, 14388, 14644, 24884, 25140, 25396, 25652, 25908, 26164, + 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 24885, 25141, 25397, 25653, 25909, 26165, + 12342, 12598, 12854, 13110, 13366, 13622, 13878, 14134, 14390, 14646, 24886, 25142, 25398, 25654, 25910, 26166, + 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, 24887, 25143, 25399, 25655, 25911, 26167, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 24888, 25144, 25400, 25656, 25912, 26168, + 12345, 12601, 12857, 13113, 13369, 13625, 13881, 14137, 14393, 14649, 24889, 25145, 25401, 25657, 25913, 26169, + 12385, 12641, 12897, 13153, 13409, 13665, 13921, 14177, 14433, 14689, 24929, 25185, 25441, 25697, 25953, 26209, + 12386, 12642, 12898, 13154, 13410, 13666, 13922, 14178, 14434, 14690, 24930, 25186, 25442, 25698, 25954, 26210, + 12387, 12643, 12899, 13155, 13411, 13667, 13923, 14179, 14435, 14691, 24931, 25187, 25443, 25699, 25955, 26211, + 12388, 12644, 12900, 13156, 13412, 13668, 13924, 14180, 14436, 14692, 24932, 25188, 25444, 25700, 25956, 26212, + 12389, 12645, 12901, 13157, 13413, 13669, 13925, 14181, 14437, 14693, 24933, 25189, 25445, 25701, 25957, 26213, + 12390, 12646, 12902, 13158, 13414, 13670, 13926, 14182, 14438, 14694, 24934, 25190, 25446, 25702, 25958, 26214 + ) + private[this] val gs: Array[Long] = Array( 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index 67eec114c..51d3d2a91 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -15,6 +15,8 @@ */ package zio.json.internal +import java.util.UUID + /** * Total, fast, number parsing. * @@ -75,13 +77,19 @@ object SafeNumbers { def toString(x: Double): String = { val out = new FastStringWrite(24) write(x, out) - out.toString + out.buffer.toString } def toString(x: Float): String = { val out = new FastStringWrite(16) write(x, out) - out.toString + out.buffer.toString + } + + def toString(x: UUID): String = { + val out = writes.get + write(x, out) + out.buffer.toString } // Based on the amazing work of Raffaello Giulietti @@ -294,6 +302,26 @@ object SafeNumbers { } } + def write(x: UUID, out: Write): Unit = { + val ds = lowerCaseHexDigits + val msb = x.getMostSignificantBits + val lsb = x.getLeastSignificantBits + val msb1 = (msb >> 32).toInt + out.write(ds(msb1 >>> 24), ds(msb1 >> 16 & 0xff), ds(msb1 >> 8 & 0xff), ds(msb1 & 0xff)) + out.write('-') + val msb2 = msb.toInt + out.write(ds(msb2 >>> 24), ds(msb2 >> 16 & 0xff)) + out.write('-') + out.write(ds(msb2 >> 8 & 0xff), ds(msb2 & 0xff)) + out.write('-') + val lsb1 = (lsb >>> 32).toInt + out.write(ds(lsb1 >>> 24), ds(lsb1 >> 16 & 0xff)) + out.write('-') + out.write(ds(lsb1 >> 8 & 0xff), ds(lsb1 & 0xff)) + val lsb2 = lsb.toInt + out.write(ds(lsb2 >>> 24), ds(lsb2 >> 16 & 0xff), ds(lsb2 >> 8 & 0xff), ds(lsb2 & 0xff)) + } + private[json] def writeNano(x: Int, out: Write): Unit = { out.write('.') var coeff = 100000000 @@ -482,6 +510,25 @@ object SafeNumbers { 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L ) + private final val lowerCaseHexDigits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 24880, 25136, 25392, 25648, 25904, 26160, + 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 24881, 25137, 25393, 25649, 25905, 26161, + 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 24882, 25138, 25394, 25650, 25906, 26162, + 12339, 12595, 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 24883, 25139, 25395, 25651, 25907, 26163, + 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, 14388, 14644, 24884, 25140, 25396, 25652, 25908, 26164, + 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 24885, 25141, 25397, 25653, 25909, 26165, + 12342, 12598, 12854, 13110, 13366, 13622, 13878, 14134, 14390, 14646, 24886, 25142, 25398, 25654, 25910, 26166, + 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, 24887, 25143, 25399, 25655, 25911, 26167, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 24888, 25144, 25400, 25656, 25912, 26168, + 12345, 12601, 12857, 13113, 13369, 13625, 13881, 14137, 14393, 14649, 24889, 25145, 25401, 25657, 25913, 26169, + 12385, 12641, 12897, 13153, 13409, 13665, 13921, 14177, 14433, 14689, 24929, 25185, 25441, 25697, 25953, 26209, + 12386, 12642, 12898, 13154, 13410, 13666, 13922, 14178, 14434, 14690, 24930, 25186, 25442, 25698, 25954, 26210, + 12387, 12643, 12899, 13155, 13411, 13667, 13923, 14179, 14435, 14691, 24931, 25187, 25443, 25699, 25955, 26211, + 12388, 12644, 12900, 13156, 13412, 13668, 13924, 14180, 14436, 14692, 24932, 25188, 25444, 25700, 25956, 26212, + 12389, 12645, 12901, 13157, 13413, 13669, 13925, 14181, 14437, 14693, 24933, 25189, 25445, 25701, 25957, 26213, + 12390, 12646, 12902, 13158, 13414, 13670, 13926, 14182, 14438, 14694, 24934, 25190, 25446, 25702, 25958, 26214 + ) + private[this] val gs: Array[Long] = Array( 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index 67eec114c..51d3d2a91 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -15,6 +15,8 @@ */ package zio.json.internal +import java.util.UUID + /** * Total, fast, number parsing. * @@ -75,13 +77,19 @@ object SafeNumbers { def toString(x: Double): String = { val out = new FastStringWrite(24) write(x, out) - out.toString + out.buffer.toString } def toString(x: Float): String = { val out = new FastStringWrite(16) write(x, out) - out.toString + out.buffer.toString + } + + def toString(x: UUID): String = { + val out = writes.get + write(x, out) + out.buffer.toString } // Based on the amazing work of Raffaello Giulietti @@ -294,6 +302,26 @@ object SafeNumbers { } } + def write(x: UUID, out: Write): Unit = { + val ds = lowerCaseHexDigits + val msb = x.getMostSignificantBits + val lsb = x.getLeastSignificantBits + val msb1 = (msb >> 32).toInt + out.write(ds(msb1 >>> 24), ds(msb1 >> 16 & 0xff), ds(msb1 >> 8 & 0xff), ds(msb1 & 0xff)) + out.write('-') + val msb2 = msb.toInt + out.write(ds(msb2 >>> 24), ds(msb2 >> 16 & 0xff)) + out.write('-') + out.write(ds(msb2 >> 8 & 0xff), ds(msb2 & 0xff)) + out.write('-') + val lsb1 = (lsb >>> 32).toInt + out.write(ds(lsb1 >>> 24), ds(lsb1 >> 16 & 0xff)) + out.write('-') + out.write(ds(lsb1 >> 8 & 0xff), ds(lsb1 & 0xff)) + val lsb2 = lsb.toInt + out.write(ds(lsb2 >>> 24), ds(lsb2 >> 16 & 0xff), ds(lsb2 >> 8 & 0xff), ds(lsb2 & 0xff)) + } + private[json] def writeNano(x: Int, out: Write): Unit = { out.write('.') var coeff = 100000000 @@ -482,6 +510,25 @@ object SafeNumbers { 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L ) + private final val lowerCaseHexDigits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 24880, 25136, 25392, 25648, 25904, 26160, + 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 24881, 25137, 25393, 25649, 25905, 26161, + 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 24882, 25138, 25394, 25650, 25906, 26162, + 12339, 12595, 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 24883, 25139, 25395, 25651, 25907, 26163, + 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, 14388, 14644, 24884, 25140, 25396, 25652, 25908, 26164, + 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 24885, 25141, 25397, 25653, 25909, 26165, + 12342, 12598, 12854, 13110, 13366, 13622, 13878, 14134, 14390, 14646, 24886, 25142, 25398, 25654, 25910, 26166, + 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, 24887, 25143, 25399, 25655, 25911, 26167, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 24888, 25144, 25400, 25656, 25912, 26168, + 12345, 12601, 12857, 13113, 13369, 13625, 13881, 14137, 14393, 14649, 24889, 25145, 25401, 25657, 25913, 26169, + 12385, 12641, 12897, 13153, 13409, 13665, 13921, 14177, 14433, 14689, 24929, 25185, 25441, 25697, 25953, 26209, + 12386, 12642, 12898, 13154, 13410, 13666, 13922, 14178, 14434, 14690, 24930, 25186, 25442, 25698, 25954, 26210, + 12387, 12643, 12899, 13155, 13411, 13667, 13923, 14179, 14435, 14691, 24931, 25187, 25443, 25699, 25955, 26211, + 12388, 12644, 12900, 13156, 13412, 13668, 13924, 14180, 14436, 14692, 24932, 25188, 25444, 25700, 25956, 26212, + 12389, 12645, 12901, 13157, 13413, 13669, 13925, 14181, 14437, 14693, 24933, 25189, 25445, 25701, 25957, 26213, + 12390, 12646, 12902, 13158, 13414, 13670, 13926, 14182, 14438, 14694, 24934, 25190, 25446, 25702, 25958, 26214 + ) + private[this] val gs: Array[Long] = Array( 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 339bc83d5..6b5045a13 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -723,12 +723,12 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { implicit val zoneId: JsonEncoder[ZoneId] = new JsonEncoder[ZoneId] { def unsafeEncode(a: ZoneId, indent: Option[Int], out: Write): Unit = { out.write('"') - serializers.write(a, out) + out.write(a.getId) out.write('"') } override final def toJsonAST(a: ZoneId): Either[String, Json] = - new Right(new Json.Str(serializers.toString(a))) + new Right(new Json.Str(a.getId)) } implicit val zoneOffset: JsonEncoder[ZoneOffset] = new JsonEncoder[ZoneOffset] { @@ -742,7 +742,16 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { new Right(new Json.Str(serializers.toString(a))) } - implicit val uuid: JsonEncoder[UUID] = stringify(_.toString) + implicit val uuid: JsonEncoder[UUID] = new JsonEncoder[UUID] { + def unsafeEncode(a: UUID, indent: Option[Int], out: Write): Unit = { + out.write('"') + SafeNumbers.write(a, out) + out.write('"') + } + + override final def toJsonAST(a: UUID): Either[String, Json] = + new Right(new Json.Str(SafeNumbers.toString(a))) + } implicit val currency: JsonEncoder[java.util.Currency] = stringify(_.toString) } From 3f7d288073c7e0ccc45a380c893b14222a215ec4 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 9 Feb 2025 11:44:05 +0100 Subject: [PATCH 150/311] More efficient strip of trailing zeros (#1296) --- .../scala/zio/json/internal/SafeNumbers.scala | 17 +++++++--------- .../scala/zio/json/internal/SafeNumbers.scala | 20 ++++++++----------- .../scala/zio/json/internal/SafeNumbers.scala | 14 +++++-------- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index d37b1369e..617538ddc 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -316,18 +316,18 @@ object SafeNumbers { val msb = x.getMostSignificantBits val lsb = x.getLeastSignificantBits val msb1 = (msb >> 32).toInt + val msb2 = msb.toInt + val lsb1 = (lsb >>> 32).toInt + val lsb2 = lsb.toInt out.write(ds(msb1 >>> 24), ds(msb1 >> 16 & 0xff), ds(msb1 >> 8 & 0xff), ds(msb1 & 0xff)) out.write('-') - val msb2 = msb.toInt out.write(ds(msb2 >>> 24), ds(msb2 >> 16 & 0xff)) out.write('-') out.write(ds(msb2 >> 8 & 0xff), ds(msb2 & 0xff)) out.write('-') - val lsb1 = (lsb >>> 32).toInt out.write(ds(lsb1 >>> 24), ds(lsb1 >> 16 & 0xff)) out.write('-') out.write(ds(lsb1 >> 8 & 0xff), ds(lsb1 & 0xff)) - val lsb2 = lsb.toInt out.write(ds(lsb2 >>> 24), ds(lsb2 >> 16 & 0xff), ds(lsb2 >> 8 & 0xff), ds(lsb2 & 0xff)) } @@ -406,14 +406,11 @@ object SafeNumbers { } @inline private[this] def stripTrailingZeros(x: Int): Int = { - var q0 = x - var q1 = 0 + var q0, q1 = x while ({ - q1 = q0 / 100 - q1 * 100 == q0 // check if q is divisible by 100 + q1 /= 10 + q1 * 10 == q0 // check if q is divisible by 100 }) q0 = q1 - q1 = q0 / 10 - if (q1 * 10 == q0) return q1 // check if q is divisible by 10 q0 } @@ -577,7 +574,7 @@ object SafeNumbers { else 10 } - private final val lowerCaseHexDigits: Array[Short] = Array( + private[this] final val lowerCaseHexDigits: Array[Short] = Array( 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 24880, 25136, 25392, 25648, 25904, 26160, 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 24881, 25137, 25393, 25649, 25905, 26161, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 24882, 25138, 25394, 25650, 25906, 26162, diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index 51d3d2a91..6e27b91c6 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -307,18 +307,18 @@ object SafeNumbers { val msb = x.getMostSignificantBits val lsb = x.getLeastSignificantBits val msb1 = (msb >> 32).toInt + val msb2 = msb.toInt + val lsb1 = (lsb >>> 32).toInt + val lsb2 = lsb.toInt out.write(ds(msb1 >>> 24), ds(msb1 >> 16 & 0xff), ds(msb1 >> 8 & 0xff), ds(msb1 & 0xff)) out.write('-') - val msb2 = msb.toInt out.write(ds(msb2 >>> 24), ds(msb2 >> 16 & 0xff)) out.write('-') out.write(ds(msb2 >> 8 & 0xff), ds(msb2 & 0xff)) out.write('-') - val lsb1 = (lsb >>> 32).toInt out.write(ds(lsb1 >>> 24), ds(lsb1 >> 16 & 0xff)) out.write('-') out.write(ds(lsb1 >> 8 & 0xff), ds(lsb1 & 0xff)) - val lsb2 = lsb.toInt out.write(ds(lsb2 >>> 24), ds(lsb2 >> 16 & 0xff), ds(lsb2 >> 8 & 0xff), ds(lsb2 & 0xff)) } @@ -369,16 +369,12 @@ object SafeNumbers { } private[this] def stripTrailingZeros(x: Int): Int = { - var q0 = x - var q1 = 0 + var q0, q1 = x while ({ - val qp = q0 * 1374389535L - q1 = (qp >> 37).toInt // divide a positive int by 100 - (qp & 0x1fc0000000L) == 0 // check if q is divisible by 100 + val qp = q1 * 3435973837L + q1 = (qp >> 35).toInt // divide a positive int by 10 + (qp & 0x7e0000000L) == 0 // check if q is divisible by 10 }) q0 = q1 - val qp = q0 * 3435973837L - q1 = (qp >> 35).toInt // divide a positive int by 10 - if ((qp & 0x7e0000000L) == 0) return q1 // check if q is divisible by 10 q0 } @@ -510,7 +506,7 @@ object SafeNumbers { 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L ) - private final val lowerCaseHexDigits: Array[Short] = Array( + private[this] final val lowerCaseHexDigits: Array[Short] = Array( 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 24880, 25136, 25392, 25648, 25904, 26160, 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 24881, 25137, 25393, 25649, 25905, 26161, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 24882, 25138, 25394, 25650, 25906, 26162, diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index 51d3d2a91..1abc290d7 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -369,16 +369,12 @@ object SafeNumbers { } private[this] def stripTrailingZeros(x: Int): Int = { - var q0 = x - var q1 = 0 + var q0, q1 = x while ({ - val qp = q0 * 1374389535L - q1 = (qp >> 37).toInt // divide a positive int by 100 - (qp & 0x1fc0000000L) == 0 // check if q is divisible by 100 + val qp = q1 * 3435973837L + q1 = (qp >> 35).toInt // divide a positive int by 10 + (qp & 0x7e0000000L) == 0 // check if q is divisible by 10 }) q0 = q1 - val qp = q0 * 3435973837L - q1 = (qp >> 35).toInt // divide a positive int by 10 - if ((qp & 0x7e0000000L) == 0) return q1 // check if q is divisible by 10 q0 } @@ -510,7 +506,7 @@ object SafeNumbers { 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L ) - private final val lowerCaseHexDigits: Array[Short] = Array( + private[this] final val lowerCaseHexDigits: Array[Short] = Array( 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 24880, 25136, 25392, 25648, 25904, 26160, 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 24881, 25137, 25393, 25649, 25905, 26161, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 24882, 25138, 25394, 25650, 25906, 26162, From 5454481d1a2fa562f9203005ee148192968daf57 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 10 Feb 2025 07:15:49 +0100 Subject: [PATCH 151/311] Update magnolia to 1.3.12 (#1298) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 1caf46a29..bcf5a104d 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Seq( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.11" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.12" ) case _ => Seq( From 92955bb8dba3b537bfe22a7d9fc7d43221f7b53c Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 10 Feb 2025 07:16:04 +0100 Subject: [PATCH 152/311] Update jsoniter-scala-core, ... to 2.33.2 (#1297) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index bcf5a104d..43581927b 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.1" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.1" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.2" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 2a0c55acfadc55048ede76df724b2d01ec196e79 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 10 Feb 2025 07:52:25 +0100 Subject: [PATCH 153/311] Fix `Json.Num.apply` for floats, doubles and `java.math.BigInteger` (#1299) --- .../src/main/scala/zio/json/ast/ast.scala | 6 +++--- .../test/scala/zio/json/ast/JsonSpec.scala | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index 264d1510f..9de0e145c 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -536,10 +536,10 @@ object Json { if (value.isValidLong) apply(value.toLong) else new Json.Num(new java.math.BigDecimal(value.bigInteger)) def apply(value: java.math.BigInteger): Num = - if (value.bitCount < 64) apply(value.longValue) + if (value.bitLength < 64) apply(value.longValue) else new Json.Num(new java.math.BigDecimal(value)) - def apply(value: Float): Num = new Num(new java.math.BigDecimal(value.toString)) - def apply(value: Double): Num = new Num(new java.math.BigDecimal(value)) + def apply(value: Float): Num = new Num(new java.math.BigDecimal(SafeNumbers.toString(value))) + def apply(value: Double): Num = new Num(new java.math.BigDecimal(SafeNumbers.toString(value))) implicit val decoder: JsonDecoder[Num] = new JsonDecoder[Num] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Num = diff --git a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala index 71cfaa7bc..a375ca08e 100644 --- a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala @@ -5,11 +5,32 @@ import zio.json._ import zio.test.Assertion._ import zio.test._ +import java.math.BigInteger + object JsonSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = suite("Json")( suite("apply")( + test("Num()") { + assertTrue(Json.Num(0).toString == "0") && + assertTrue(Json.Num(0.0).toString == "0.0") && + assertTrue(Json.Num(1.0).toString == "1.0") && + assertTrue(Json.Num(-0.0).toString == "0.0") && + assertTrue(Json.Num(-1.0).toString == "-1.0") && + assertTrue(Json.Num(7: Byte).toString == "7") && + assertTrue(Json.Num(777: Short).toString == "777") && + assertTrue(Json.Num(123456789).toString == "123456789") && + assertTrue(Json.Num(1.2345678f).toString == "1.2345678") && + assertTrue(Json.Num(1.2345678901234567).toString == "1.2345678901234567") && + assertTrue(Json.Num(1234567890123456789L).toString == "1234567890123456789") && + assertTrue(Json.Num(BigInteger.valueOf(1234567890123456789L)).toString == "1234567890123456789") && + assertTrue(Json.Num(new BigInteger("12345678901234567890")).toString == "12345678901234567890") && + assertTrue(Json.Num(BigInt(1234567890123456789L)).toString == "1234567890123456789") && + assertTrue(Json.Num(BigInt("12345678901234567890")).toString == "12345678901234567890") && + assertTrue(Json.Num(BigDecimal(1234567890123456789L)).toString == "1234567890123456789") && + assertTrue(Json.Num(BigDecimal("12345678901234567890")).toString == "12345678901234567890") + }, test("Bool()") { assertTrue(Json.Bool.True eq Json.Bool(true)) && assertTrue(Json.Bool.False eq Json.Bool(false)) From d214beb59c3879b5f1688c5f97f00e34bf144bac Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 11 Feb 2025 10:47:29 +0100 Subject: [PATCH 154/311] Use 256-bit limitation for mantissa length when parsing floating point numbers (#1302) --- docs/security.md | 2 +- .../filteredgentype/FilteredGenType.json | 200 +++++++++--------- .../scala/zio/json/golden/GoldenSpec.scala | 4 +- .../scala/zio/json/internal/SafeNumbers.scala | 10 +- .../zio/json/internal/UnsafeNumbers.scala | 34 ++- .../json/internal/SafeNumbersBenchmarks.scala | 4 +- .../scala/zio/json/internal/SafeNumbers.scala | 10 +- .../zio/json/internal/UnsafeNumbers.scala | 34 ++- .../scala/zio/json/data/geojson/GeoJSON.scala | 2 +- .../scala/zio/json/internal/SafeNumbers.scala | 10 +- .../zio/json/internal/UnsafeNumbers.scala | 34 ++- .../main/scala/zio/json/internal/lexer.scala | 5 +- .../src/test/scala/zio/json/CodecSpec.scala | 2 +- .../src/test/scala/zio/json/DecoderSpec.scala | 16 +- .../src/test/scala/zio/json/EncoderSpec.scala | 70 +++++- .../shared/src/test/scala/zio/json/Gens.scala | 8 +- .../zio/json/internal/SafeNumbersSpec.scala | 11 +- 17 files changed, 258 insertions(+), 198 deletions(-) diff --git a/docs/security.md b/docs/security.md index 194b5cfda..051b6b342 100644 --- a/docs/security.md +++ b/docs/security.md @@ -94,4 +94,4 @@ circe 4529 ( 7456) 2037 (1533) This attack is very effective in schemas with lots of numbers, causing ops/sec to be halved with a 33% increase in memory usage. -`zio-json` is resistant to a wide range of number based attacks because it uses a from-scratch number parser that will exit early when the number of bits of any number exceeds 128 bits, which can be customized by the system property `zio.json.number.bits`. +`zio-json` is resistant to a wide range of number based attacks because it uses a from-scratch number parser that will exit early when the number of bits of any number exceeds 256 bits. diff --git a/zio-json-golden/src/test/resources/golden/filteredgentype/FilteredGenType.json b/zio-json-golden/src/test/resources/golden/filteredgentype/FilteredGenType.json index cbff71868..f5eed5eb3 100644 --- a/zio-json-golden/src/test/resources/golden/filteredgentype/FilteredGenType.json +++ b/zio-json-golden/src/test/resources/golden/filteredgentype/FilteredGenType.json @@ -1,304 +1,304 @@ { "samples" : [ { - "a" : -6.807778209396064042484608633080144E+37 + "a" : -2.316566882566080907224842077045326E+76 }, { - "a" : 1.595098341748072691735235811828587E+38 + "a" : -4.569412983810212758054894223077703E+76 }, { - "a" : 1.459950313422007732470858735428470E+38 + "a" : 5.228989702464284595177704658722007E+76 }, { - "a" : 3.966890979309949040353919413508109E+37 + "a" : 2.548479930525632094981978203969745E+76 }, { - "a" : 1.233609631287946015973115353212111E+38 + "a" : 1.374041878513211278247992702527512E+76 }, { - "a" : 1.473182672254656075288635468340979E+38 + "a" : 1.366736473169616603716555534546644E+76 }, { - "a" : -4.370307886719313347409020693679809E+36 + "a" : 3.911179114719388819861885552560843E+76 }, { - "a" : 4.037946164963809325160258151515959E+37 + "a" : 1.944890681930929342312977988211045E+76 }, { - "a" : 9.654807468219847939517983223821157E+36 + "a" : 4.465885331138848906986116797747655E+76 }, { - "a" : 9.016039177425153953741714882797374E+37 + "a" : 3.006637389783933263445463075262541E+76 }, { - "a" : 7.146160681625873270982431039786890E+37 + "a" : -5.751649455858367893815313696913694E+76 }, { - "a" : 5.715520023941784610636805140974297E+37 + "a" : -1.784887552001718903374836951202540E+76 }, { - "a" : -4.963886209068154507584885110622067E+37 + "a" : -4.910947695145483225976445529943760E+76 }, { - "a" : 1.429642469253121453233931676540811E+38 + "a" : -1.457349100691995363983649450959469E+76 }, { - "a" : 1.347358173024022215551559165152039E+38 + "a" : -2.173117328253411917053167250124104E+76 }, { - "a" : -1.443197818206812225540268705601235E+38 + "a" : -3.816709640530119614187277451466508E+76 }, { - "a" : 1.652974714985098210216497615438388E+38 + "a" : -3.324016269398039518561448095600154E+76 }, { - "a" : 6.854881439246725349714811941975828E+37 + "a" : 2.133340242534658987737410780967371E+76 }, { - "a" : -1.440269453811987772460377566176060E+38 + "a" : 2.899391869007164686595655383666939E+75 }, { - "a" : 7.349647466961309857751700891214103E+37 + "a" : 1.289089776333850543053015914013214E+75 }, { - "a" : 1.442405212658813209066752593353578E+38 + "a" : -3.245057840870445841628197448179636E+75 }, { - "a" : 3.965771366281331473297193084703031E+37 + "a" : 4.164835378606767954973150783579233E+76 }, { - "a" : 6.269323508703350877363225990119655E+37 + "a" : -3.770560172944845271964280390072137E+76 }, { - "a" : -1.758914181069706042842822199462931E+37 + "a" : 5.112438700963875953568847807288529E+76 }, { - "a" : 1.223935114914264676851645426397511E+38 + "a" : -4.522549082517651900179843809145666E+76 }, { - "a" : -7.384928634462281384291440360868614E+37 + "a" : 4.412399638137604448664402635608744E+76 }, { - "a" : 1.693228930919227030554211724500322E+38 + "a" : 1.514016982543592890366775343872403E+76 }, { - "a" : 4.424974721701017780262797468032916E+37 + "a" : -8.440138198169034012113061607781643E+75 }, { - "a" : -1.303664216711215462163176389172558E+38 + "a" : 8.692134402815438942477068350566858E+75 }, { - "a" : 1.404704317795160461166424204194676E+37 + "a" : 1.784909366617606219860919821779445E+75 }, { - "a" : -1.329057724454004799664476545910425E+38 + "a" : -3.207891411756439559673859312030194E+76 }, { - "a" : -1.048356224872675570271942686628903E+38 + "a" : 4.617070445357631467547656360854253E+76 }, { - "a" : -6.290438873817500525528580887751261E+37 + "a" : 3.901654475403585079437303428377294E+75 }, { - "a" : 1.084524705805485052810712065828027E+38 + "a" : 1.809738335539952279801907689220288E+75 }, { - "a" : -7.550243566246841354983346269026909E+37 + "a" : 1.867508850719084820991569833616372E+76 }, { - "a" : 9.340651914763911687206557126706644E+37 + "a" : -4.487014217306435076482810842738905E+76 }, { - "a" : -2.480333693026775008020223168834843E+37 + "a" : -3.483434481661576170222377673573347E+76 }, { - "a" : -1.094510132373439403344708257152704E+38 + "a" : -3.867693189336099364999112808463955E+76 }, { - "a" : -6.786566488714105384835344334324745E+37 + "a" : 3.097928175532344917894163172264977E+76 }, { - "a" : 1.584399487952599918984676750023353E+37 + "a" : -5.057812661184695904653860723677742E+76 }, { - "a" : 5.128682031007228158346770228279368E+37 + "a" : 4.968641799647727075789350301886275E+75 }, { - "a" : 9.348340016921799959253942302421099E+37 + "a" : -1.496654796873222027485144992012494E+76 }, { - "a" : 7.030935368599146218716189376901462E+37 + "a" : -2.496076752945529018769889843804460E+76 }, { - "a" : 1.535594588509527431040574927018135E+38 + "a" : -3.067118696151703815163256919101265E+76 }, { - "a" : -4.934741325633573501964215791643639E+37 + "a" : 3.412873919590638540319289335182708E+76 }, { - "a" : 9.682312857134232428245162228518594E+37 + "a" : 1.480433219806823809604660703733397E+76 }, { - "a" : -2.614986691569376906824897939793061E+37 + "a" : -6.339658483521043521286676489621022E+75 }, { - "a" : -1.014394404488501605028181495043210E+38 + "a" : -4.114346648557266778860963307677252E+76 }, { - "a" : 5.488115260327267108034268022746715E+37 + "a" : 8.097697147044163995162055017419827E+75 }, { - "a" : 1.126087665079675493640367522377595E+38 + "a" : 2.214651974619448279825666523589333E+76 }, { - "a" : -1.039540496026219616948722255179099E+38 + "a" : -3.574516354020052112075424495657422E+76 }, { - "a" : -9.140747409014218126917907727764322E+37 + "a" : 1.104360909238067990458557751532528E+76 }, { - "a" : -4.824125603843691307227014286009879E+37 + "a" : 1.399684847297342561050267725918531E+76 }, { - "a" : -8.145094645621006028171195553277435E+37 + "a" : 2.502156861243295972203472905374441E+76 }, { - "a" : -5.626674097898124181091040103617824E+37 + "a" : -1.604836447654289039403981953467744E+76 }, { - "a" : 9.103992673978668331401073051758253E+37 + "a" : 1.658811782314510920715101305263320E+76 }, { - "a" : -6.434701867932310680029743191675950E+37 + "a" : 5.139354255763948897207867902940651E+76 }, { - "a" : 1.544388056858114009506299836014695E+37 + "a" : 3.575018689737287736972267117825426E+76 }, { - "a" : -1.265122848783281340816555691730657E+38 + "a" : 5.210629430128216788313314937720483E+76 }, { - "a" : -1.038801113752491268000223564191006E+38 + "a" : 2.554485802500375585530452513390781E+76 }, { - "a" : 1.005113855834286665723509372286190E+38 + "a" : 5.272520867084473865402574088199686E+76 }, { - "a" : -1.532637041640666300354819845360303E+38 + "a" : 2.947116531100159457188114248938062E+76 }, { - "a" : 1.460152591686346812838507724273444E+37 + "a" : 5.606673728406112706411336596318572E+76 }, { - "a" : -1.807215480356152323767541674888201E+37 + "a" : -2.738195022969698014068611960296754E+76 }, { - "a" : 1.130164367464931673447884683370859E+38 + "a" : -4.988632907332175914225601107044307E+76 }, { - "a" : -2.206605703514989373349781667972389E+37 + "a" : 4.333006311262914052378138242825969E+76 }, { - "a" : -7.335310305765769880508310438610933E+37 + "a" : 1.811966148571775577237795257899742E+76 }, { - "a" : -9.032060789190847023210378392783773E+37 + "a" : 3.532034121631590266281611826625215E+76 }, { - "a" : -1.434866879379575685137940146522389E+38 + "a" : -3.030563670404741512818043914917342E+76 }, { - "a" : -8.537143289319745801149293653774310E+37 + "a" : 3.296229785246419707095000587083274E+76 }, { - "a" : -7.189751213495862580473152406825669E+37 + "a" : -2.181511118190992226401258245868472E+75 }, { - "a" : -1.116003336173084398830455517338484E+38 + "a" : -5.248524012170844440886915873124918E+76 }, { - "a" : 4.350602216631428056662100784595331E+37 + "a" : -2.710770101040726300947437430936575E+76 }, { - "a" : -4.200580225109217264680432341389498E+37 + "a" : -1.943466455385574430218910331374480E+76 }, { - "a" : 1.519464406997130639901449647551254E+38 + "a" : -1.374906724157606925294570143297134E+76 }, { - "a" : 1.605847304967111250934189072078944E+38 + "a" : 4.043622418717921248146223721034532E+76 }, { - "a" : -1.537207767079682775719785647758733E+38 + "a" : 2.647781320119073554660652286568971E+76 }, { - "a" : -1.279659726955884472452164873447049E+38 + "a" : 4.785326654831060667212354593166055E+76 }, { - "a" : 6.508277213006463765700394170604852E+37 + "a" : -2.393552079769908150471681607119720E+76 }, { - "a" : 1.283817488073224657514525683762202E+38 + "a" : -5.137463583987553425511474469196396E+74 }, { - "a" : 1.339367218367240868953919097436571E+38 + "a" : 1.067970096951755185859241750338926E+76 }, { - "a" : -1.682265900214633524445901815287998E+37 + "a" : 4.462068429365855045664774983601338E+76 }, { - "a" : 1.206863685039632397166752202964046E+38 + "a" : 4.632622188172519290610657636467222E+76 }, { - "a" : -1.084738675922968416633463809761249E+38 + "a" : 2.461239593255391819307695081683695E+76 }, { - "a" : 5.657051930790282199429825000570743E+37 + "a" : 1.729396380088878041781707741829649E+76 }, { - "a" : -8.295559405291592878379291027574690E+36 + "a" : 4.211133948578506902750612745348381E+76 }, { - "a" : 1.759196447669202351875403477421470E+37 + "a" : 4.588026434593706190135184314006573E+76 }, { - "a" : 1.469253810134078897550508376470893E+38 + "a" : 1.463113899048768702370675156356965E+76 }, { - "a" : 4.042754662729660664430342317410895E+37 + "a" : -6.211105372002631227089153408882691E+75 }, { - "a" : -2.203950127770040611464116843938096E+37 + "a" : -1.067219487693268067286110477325699E+76 }, { - "a" : 1.510320473631250916474309414975180E+38 + "a" : -3.186432400285580393171287587355880E+76 }, { - "a" : -7.763024290460987157407648151549977E+37 + "a" : -1.698453401490616715439518694710733E+76 }, { - "a" : 2.621985659558485942857886646450931E+37 + "a" : -4.746614822847365827886773917395646E+76 }, { - "a" : -7.736585181363804829301628996321853E+37 + "a" : 5.405374454168044475426989745354982E+76 }, { - "a" : -6.009164305461593191654380749583999E+37 + "a" : 3.493241979655892720402456230475866E+76 }, { - "a" : -9.519320687479863998450437188934807E+37 + "a" : -4.923252250897307532574828650812151E+76 }, { - "a" : -3.199912116712397916450907359672628E+37 + "a" : -4.011060043301704192745191895787696E+76 }, { - "a" : 1.050603568467569482115229536968020E+38 + "a" : -5.689109292584424095652541096534457E+76 }, { - "a" : -1.259168689102438672889911917677648E+38 + "a" : 1.636983807742317199740108642526352E+76 }, { - "a" : -1.203951555022847927293048112388223E+38 + "a" : -2.083099272612476220093157442600918E+75 } ] } \ No newline at end of file diff --git a/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala b/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala index 89eb98d92..4f609e433 100644 --- a/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala +++ b/zio-json-golden/src/test/scala/zio/json/golden/GoldenSpec.scala @@ -40,9 +40,9 @@ object GoldenSpec extends ZIOSpecDefault { */ val genBigDecimal: Gen[Any, java.math.BigDecimal] = Gen - .bigDecimal((BigDecimal(2).pow(128) - 1) * -1, BigDecimal(2).pow(128) - 1) + .bigDecimal((BigDecimal(2).pow(256) - 1) * -1, BigDecimal(2).pow(256) - 1) .map(_.bigDecimal) - .filter(_.toBigInteger.bitLength < 128) + .filter(_.toBigInteger.bitLength < 256) genBigDecimal.map(FilteredGenType.apply) } diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 617538ddc..513f19795 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -35,7 +35,7 @@ import java.util.UUID * "1.000e-5", which is useful in cases where the trailing zeros denote measurement accuracy. * * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit limit on the size of the significand, to - * avoid OOM style attacks, which is 128 bits by default. + * avoid OOM style attacks, which is 256 bits by default. * * Results are contained in a specialisation of Option that avoids boxing. */ @@ -58,19 +58,19 @@ object SafeNumbers { try LongSome(UnsafeNumbers.long(num)) catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] = + def bigInteger(num: String, max_bits: Int = 256): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } - def float(num: String, max_bits: Int = 128): FloatOption = + def float(num: String, max_bits: Int = 256): FloatOption = try FloatSome(UnsafeNumbers.float(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => FloatNone } - def double(num: String, max_bits: Int = 128): DoubleOption = + def double(num: String, max_bits: Int = 256): DoubleOption = try DoubleSome(UnsafeNumbers.double(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] = + def bigDecimal(num: String, max_bits: Int = 256): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index b358ffb6b..70bb1b778 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -254,17 +254,14 @@ object UnsafeNumbers { var current = if (consume) in.readChar().toInt else in.nextNonWhitespace().toInt - if (current == 'N') { + val negate = current == '-' + if (negate) current = in.readChar().toInt + else if (current == 'N') { readAll(in, "aN", consume) return Float.NaN } - val negate = current == '-' - if (negate) current = in.readChar().toInt if (current == 'I' || current == '+') { - if (current == '+') { - current = in.readChar().toInt - if (current != 'I') throw UnsafeNumber - } + if (current == '+' && in.readChar() != 'I') throw UnsafeNumber readAll(in, "nfinity", consume) return if (negate) Float.NegativeInfinity else Float.PositiveInfinity } @@ -327,7 +324,7 @@ object UnsafeNumbers { } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { - var x: Float = + var x = if (e10 == 0) loM10.toFloat else { if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { @@ -339,7 +336,7 @@ object UnsafeNumbers { if (negate) x = -x return x } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -372,7 +369,7 @@ object UnsafeNumbers { else if (e2 >= 105) 0x7f800000 else e2 + 150 << 23 | mf & 0x7fffff } - else java.math.BigDecimal.valueOf(m10, -e10).floatValue() + else java.math.BigDecimal.valueOf(m10, -e10).floatValue } def double(num: String, max_bits: Int): Double = @@ -382,17 +379,14 @@ object UnsafeNumbers { var current = if (consume) in.readChar().toInt else in.nextNonWhitespace().toInt - if (current == 'N') { + val negate = current == '-' + if (negate) current = in.readChar().toInt + else if (current == 'N') { readAll(in, "aN", consume) return Double.NaN } - val negate = current == '-' - if (negate) current = in.readChar().toInt if (current == 'I' || current == '+') { - if (current == '+') { - current = in.readChar().toInt - if (current != 'I') throw UnsafeNumber - } + if (current == '+' && in.readChar() != 'I') throw UnsafeNumber readAll(in, "nfinity", consume) return if (negate) Double.NegativeInfinity else Double.PositiveInfinity } @@ -455,7 +449,7 @@ object UnsafeNumbers { } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { - var x: Double = + var x = if (e10 == 0) loM10.toDouble else { if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { @@ -471,7 +465,7 @@ object UnsafeNumbers { if (negate) x = -x return x } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -504,7 +498,7 @@ object UnsafeNumbers { else if (e2 >= 972) 0x7ff0000000000000L else (e2 + 1075).toLong << 52 | m2 & 0xfffffffffffffL } - else java.math.BigDecimal.valueOf(m10, -e10).doubleValue() + else java.math.BigDecimal.valueOf(m10, -e10).doubleValue } @noinline private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { diff --git a/zio-json/jvm/src/jmh/scala/zio/json/internal/SafeNumbersBenchmarks.scala b/zio-json/jvm/src/jmh/scala/zio/json/internal/SafeNumbersBenchmarks.scala index 18bf3a5a0..345b8afa0 100644 --- a/zio-json/jvm/src/jmh/scala/zio/json/internal/SafeNumbersBenchmarks.scala +++ b/zio-json/jvm/src/jmh/scala/zio/json/internal/SafeNumbersBenchmarks.scala @@ -115,7 +115,7 @@ class SafeNumbersBenchFloat { @Benchmark def decodeFommilUnsafeValid(): Array[Float] = - valids.map(UnsafeNumbers.float(_, 128)) + valids.map(UnsafeNumbers.float(_, 256)) @Benchmark def decodeStdlibInvalid(): Array[FloatOption] = invalids.map(stdlib) @@ -182,7 +182,7 @@ class SafeNumbersBenchBigDecimal { @Benchmark def decodeFommilUnsafeValid(): Array[java.math.BigDecimal] = - valids.map(UnsafeNumbers.bigDecimal(_, 128)) + valids.map(UnsafeNumbers.bigDecimal(_, 256)) @Benchmark def decodeStdlibInvalid(): Array[Option[java.math.BigDecimal]] = diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala index 6e27b91c6..e6a72b054 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -35,7 +35,7 @@ import java.util.UUID * "1.000e-5", which is useful in cases where the trailing zeros denote measurement accuracy. * * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit limit on the size of the significand, to - * avoid OOM style attacks, which is 128 bits by default. + * avoid OOM style attacks, which is 256 bits by default. * * Results are contained in a specialisation of Option that avoids boxing. */ @@ -58,19 +58,19 @@ object SafeNumbers { try LongSome(UnsafeNumbers.long(num)) catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] = + def bigInteger(num: String, max_bits: Int = 256): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } - def float(num: String, max_bits: Int = 128): FloatOption = + def float(num: String, max_bits: Int = 256): FloatOption = try FloatSome(UnsafeNumbers.float(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => FloatNone } - def double(num: String, max_bits: Int = 128): DoubleOption = + def double(num: String, max_bits: Int = 256): DoubleOption = try DoubleSome(UnsafeNumbers.double(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] = + def bigDecimal(num: String, max_bits: Int = 256): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index a41908157..ce269ae7a 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -254,17 +254,14 @@ object UnsafeNumbers { var current = if (consume) in.readChar().toInt else in.nextNonWhitespace().toInt - if (current == 'N') { + val negate = current == '-' + if (negate) current = in.readChar().toInt + else if (current == 'N') { readAll(in, "aN", consume) return Float.NaN } - val negate = current == '-' - if (negate) current = in.readChar().toInt if (current == 'I' || current == '+') { - if (current == '+') { - current = in.readChar().toInt - if (current != 'I') throw UnsafeNumber - } + if (current == '+' && in.readChar() != 'I') throw UnsafeNumber readAll(in, "nfinity", consume) return if (negate) Float.NegativeInfinity else Float.PositiveInfinity } @@ -327,7 +324,7 @@ object UnsafeNumbers { } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { - var x: Float = + var x = if (e10 == 0) loM10.toFloat else { if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { @@ -339,7 +336,7 @@ object UnsafeNumbers { if (negate) x = -x return x } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -372,7 +369,7 @@ object UnsafeNumbers { else if (e2 >= 105) 0x7f800000 else e2 + 150 << 23 | mf & 0x7fffff } - else java.math.BigDecimal.valueOf(m10, -e10).floatValue() + else java.math.BigDecimal.valueOf(m10, -e10).floatValue } def double(num: String, max_bits: Int): Double = @@ -382,17 +379,14 @@ object UnsafeNumbers { var current = if (consume) in.readChar().toInt else in.nextNonWhitespace().toInt - if (current == 'N') { + val negate = current == '-' + if (negate) current = in.readChar().toInt + else if (current == 'N') { readAll(in, "aN", consume) return Double.NaN } - val negate = current == '-' - if (negate) current = in.readChar().toInt if (current == 'I' || current == '+') { - if (current == '+') { - current = in.readChar().toInt - if (current != 'I') throw UnsafeNumber - } + if (current == '+' && in.readChar() != 'I') throw UnsafeNumber readAll(in, "nfinity", consume) return if (negate) Double.NegativeInfinity else Double.PositiveInfinity } @@ -455,7 +449,7 @@ object UnsafeNumbers { } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { - var x: Double = + var x = if (e10 == 0) loM10.toDouble else { if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { @@ -471,7 +465,7 @@ object UnsafeNumbers { if (negate) x = -x return x } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -504,7 +498,7 @@ object UnsafeNumbers { else if (e2 >= 972) 0x7ff0000000000000L else (e2 + 1075).toLong << 52 | m2 & 0xfffffffffffffL } - else java.math.BigDecimal.valueOf(m10, -e10).doubleValue() + else java.math.BigDecimal.valueOf(m10, -e10).doubleValue } @noinline private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { diff --git a/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala index 9cbca8d9b..b827fd66b 100644 --- a/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala +++ b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala @@ -156,7 +156,7 @@ package handrolled { js match { case Json.Arr(chunk) if chunk.length == 2 && chunk(0).isInstanceOf[Json.Num] && chunk(1).isInstanceOf[Json.Num] => - (chunk(0).asInstanceOf[Json.Num].value.doubleValue(), chunk(1).asInstanceOf[Json.Num].value.doubleValue()) + (chunk(0).asInstanceOf[Json.Num].value.doubleValue, chunk(1).asInstanceOf[Json.Num].value.doubleValue) case _ => Lexer.error("expected coordinates", trace) } def coordinates1( diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala index 1abc290d7..547918b9e 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -35,7 +35,7 @@ import java.util.UUID * "1.000e-5", which is useful in cases where the trailing zeros denote measurement accuracy. * * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit limit on the size of the significand, to - * avoid OOM style attacks, which is 128 bits by default. + * avoid OOM style attacks, which is 256 bits by default. * * Results are contained in a specialisation of Option that avoids boxing. */ @@ -58,19 +58,19 @@ object SafeNumbers { try LongSome(UnsafeNumbers.long(num)) catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] = + def bigInteger(num: String, max_bits: Int = 256): Option[java.math.BigInteger] = try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } - def float(num: String, max_bits: Int = 128): FloatOption = + def float(num: String, max_bits: Int = 256): FloatOption = try FloatSome(UnsafeNumbers.float(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => FloatNone } - def double(num: String, max_bits: Int = 128): DoubleOption = + def double(num: String, max_bits: Int = 256): DoubleOption = try DoubleSome(UnsafeNumbers.double(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] = + def bigDecimal(num: String, max_bits: Int = 256): Option[java.math.BigDecimal] = try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index a41908157..ce269ae7a 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -254,17 +254,14 @@ object UnsafeNumbers { var current = if (consume) in.readChar().toInt else in.nextNonWhitespace().toInt - if (current == 'N') { + val negate = current == '-' + if (negate) current = in.readChar().toInt + else if (current == 'N') { readAll(in, "aN", consume) return Float.NaN } - val negate = current == '-' - if (negate) current = in.readChar().toInt if (current == 'I' || current == '+') { - if (current == '+') { - current = in.readChar().toInt - if (current != 'I') throw UnsafeNumber - } + if (current == '+' && in.readChar() != 'I') throw UnsafeNumber readAll(in, "nfinity", consume) return if (negate) Float.NegativeInfinity else Float.PositiveInfinity } @@ -327,7 +324,7 @@ object UnsafeNumbers { } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { - var x: Float = + var x = if (e10 == 0) loM10.toFloat else { if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { @@ -339,7 +336,7 @@ object UnsafeNumbers { if (negate) x = -x return x } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -372,7 +369,7 @@ object UnsafeNumbers { else if (e2 >= 105) 0x7f800000 else e2 + 150 << 23 | mf & 0x7fffff } - else java.math.BigDecimal.valueOf(m10, -e10).floatValue() + else java.math.BigDecimal.valueOf(m10, -e10).floatValue } def double(num: String, max_bits: Int): Double = @@ -382,17 +379,14 @@ object UnsafeNumbers { var current = if (consume) in.readChar().toInt else in.nextNonWhitespace().toInt - if (current == 'N') { + val negate = current == '-' + if (negate) current = in.readChar().toInt + else if (current == 'N') { readAll(in, "aN", consume) return Double.NaN } - val negate = current == '-' - if (negate) current = in.readChar().toInt if (current == 'I' || current == '+') { - if (current == '+') { - current = in.readChar().toInt - if (current != 'I') throw UnsafeNumber - } + if (current == '+' && in.readChar() != 'I') throw UnsafeNumber readAll(in, "nfinity", consume) return if (negate) Double.NegativeInfinity else Double.PositiveInfinity } @@ -455,7 +449,7 @@ object UnsafeNumbers { } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { - var x: Double = + var x = if (e10 == 0) loM10.toDouble else { if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { @@ -471,7 +465,7 @@ object UnsafeNumbers { if (negate) x = -x return x } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue() + toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -504,7 +498,7 @@ object UnsafeNumbers { else if (e2 >= 972) 0x7ff0000000000000L else (e2 + 1075).toLong << 52 | m2 & 0xfffffffffffffL } - else java.math.BigDecimal.valueOf(m10, -e10).doubleValue() + else java.math.BigDecimal.valueOf(m10, -e10).doubleValue } @noinline private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index aa4988dae..5628ec091 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -308,7 +308,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits bit BigInteger", trace) + case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits-bit BigInteger", trace) } def float(trace: List[JsonError], in: RetractReader): Float = @@ -335,7 +335,7 @@ object Lexer { in.retract() i } catch { - case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits BigDecimal", trace) + case UnsafeNumbers.UnsafeNumber => error(s"expected a BigDecimal with $NumberMaxBits-bit mantissa", trace) } @inline def char(trace: List[JsonError], in: OneCharReader, c: Char): Unit = { @@ -343,6 +343,7 @@ object Lexer { if (got != c) error(s"'$c'", got, trace) } + // FIXME: remove on next major version release @inline def charOnly(trace: List[JsonError], in: OneCharReader, c: Char): Unit = { val got = in.readChar() if (got != c) error(s"'$c'", got, trace) diff --git a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala index 60f002c1a..a785de23e 100644 --- a/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/CodecSpec.scala @@ -40,7 +40,7 @@ object CodecSpec extends ZIOSpecDefault { "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" .fromJson[java.math.BigInteger] )( - isLeft(equalTo("(expected a 256 bit BigInteger)")) + isLeft(equalTo("(expected a 256-bit BigInteger)")) ) }, test("java.util.Currency") { diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index ba61bc278..7fbbc4f1b 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -106,6 +106,20 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue("\"-Infinity\"".fromJson[BigDecimal].isLeft) && assertTrue("\"NaN\"".fromJson[BigDecimal].isLeft) }, + test("BigDecimal from JSON AST") { + assert("13.38885989999999992505763657391071319580078125".fromJson[Json])( + isRight(equalTo(Json.Num(BigDecimal("13.38885989999999992505763657391071319580078125")))) + ) + }, + test("BigDecimal too large") { + // this big integer consumes more than 256 bits + assert( + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" + .fromJson[BigDecimal] + )( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) + }, test("BigInteger") { assert("170141183460469231731687303715884105728".fromJson[BigInteger])( isRight(equalTo(new BigInteger("170141183460469231731687303715884105728"))) @@ -124,7 +138,7 @@ object DecoderSpec extends ZIOSpecDefault { "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" .fromJson[java.math.BigInteger] )( - isLeft(equalTo("(expected a 256 bit BigInteger)")) + isLeft(equalTo("(expected a 256-bit BigInteger)")) ) }, test("collections") { diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index d7421debb..278512cd2 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -15,13 +15,13 @@ object EncoderSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = suite("Encoder")( suite("toJson")( + test("strings") { + assert("hello world".toJson)(equalTo("\"hello world\"")) && + assert("hello\nworld".toJson)(equalTo("\"hello\\nworld\"")) && + assert("hello\rworld".toJson)(equalTo("\"hello\\rworld\"")) && + assert("hello\u0000world".toJson)(equalTo("\"hello\\u0000world\"")) + }, suite("primitives")( - test("strings") { - assert("hello world".toJson)(equalTo("\"hello world\"")) && - assert("hello\nworld".toJson)(equalTo("\"hello\\nworld\"")) && - assert("hello\rworld".toJson)(equalTo("\"hello\\rworld\"")) && - assert("hello\u0000world".toJson)(equalTo("\"hello\\u0000world\"")) - }, test("boolean") { assert(true.toJson)(equalTo("true")) && assert(false.toJson)(equalTo("false")) @@ -30,6 +30,63 @@ object EncoderSpec extends ZIOSpecDefault { assert('c'.toJson)(equalTo("\"c\"")) && assert(Symbol("c").toJson)(equalTo("\"c\"")) }, + test("byte") { + assert((0: Short).toJson)(equalTo("0")) && + assert((1: Short).toJson)(equalTo("1")) && + assert((12: Short).toJson)(equalTo("12")) && + assert((123: Short).toJson)(equalTo("123")) && + assert((127: Short).toJson)(equalTo("127")) && + assert((-128: Short).toJson)(equalTo("-128")) + }, + test("short") { + assert((0: Short).toJson)(equalTo("0")) && + assert((1: Short).toJson)(equalTo("1")) && + assert((12: Short).toJson)(equalTo("12")) && + assert((123: Short).toJson)(equalTo("123")) && + assert((1234: Short).toJson)(equalTo("1234")) && + assert((12345: Short).toJson)(equalTo("12345")) && + assert((32767: Short).toJson)(equalTo("32767")) && + assert((-32768: Short).toJson)(equalTo("-32768")) + }, + test("int") { + assert(0.toJson)(equalTo("0")) && + assert(1.toJson)(equalTo("1")) && + assert(12.toJson)(equalTo("12")) && + assert(123.toJson)(equalTo("123")) && + assert(1234.toJson)(equalTo("1234")) && + assert(12345.toJson)(equalTo("12345")) && + assert(123456.toJson)(equalTo("123456")) && + assert(1234567.toJson)(equalTo("1234567")) && + assert(12345678.toJson)(equalTo("12345678")) && + assert(123456789.toJson)(equalTo("123456789")) && + assert(1234567890.toJson)(equalTo("1234567890")) && + assert(2147483647.toJson)(equalTo("2147483647")) && + assert(-2147483648.toJson)(equalTo("-2147483648")) + }, + test("long") { + assert(0L.toJson)(equalTo("0")) && + assert(1L.toJson)(equalTo("1")) && + assert(12L.toJson)(equalTo("12")) && + assert(123L.toJson)(equalTo("123")) && + assert(1234L.toJson)(equalTo("1234")) && + assert(12345L.toJson)(equalTo("12345")) && + assert(123456L.toJson)(equalTo("123456")) && + assert(1234567L.toJson)(equalTo("1234567")) && + assert(12345678L.toJson)(equalTo("12345678")) && + assert(123456789L.toJson)(equalTo("123456789")) && + assert(1234567890L.toJson)(equalTo("1234567890")) && + assert(12345678901L.toJson)(equalTo("12345678901")) && + assert(123456789012L.toJson)(equalTo("123456789012")) && + assert(1234567890123L.toJson)(equalTo("1234567890123")) && + assert(12345678901234L.toJson)(equalTo("12345678901234")) && + assert(123456789012345L.toJson)(equalTo("123456789012345")) && + assert(1234567890123456L.toJson)(equalTo("1234567890123456")) && + assert(12345678901234567L.toJson)(equalTo("12345678901234567")) && + assert(123456789012345678L.toJson)(equalTo("123456789012345678")) && + assert(1234567890123456789L.toJson)(equalTo("1234567890123456789")) && + assert(9223372036854775807L.toJson)(equalTo("9223372036854775807")) && + assert(-9223372036854775808L.toJson)(equalTo("-9223372036854775808")) + }, test("float") { assert(Float.NaN.toJson)(equalTo("\"NaN\"")) && assert(Float.PositiveInfinity.toJson)(equalTo("\"Infinity\"")) && @@ -137,6 +194,7 @@ object EncoderSpec extends ZIOSpecDefault { assert(7.1202363472230444e-307d.toJson)(equalTo("7.120236347223045E-307")) && assert(3.67301024534615e16d.toJson)(equalTo("3.67301024534615E16")) && assert(5.9604644775390625e-8d.toJson)(equalTo("5.960464477539063E-8")) && + assert(5.829003601188985e15d.toJson)(equalTo("5.829003601188985E15")) && assert(1.0e-322d.toJson)(equalTo("9.9E-323")) && // 20 * 2 ^ -1074 == 9.88... * 10 ^ -323 assert(5.0e-324d.toJson)(equalTo("4.9E-324")) && // 1 * 2 ^ -1074 == 4.94... * 10 ^ -324 assert(1.0e23d.toJson)( diff --git a/zio-json/shared/src/test/scala/zio/json/Gens.scala b/zio-json/shared/src/test/scala/zio/json/Gens.scala index e9d5c4ea3..417956559 100644 --- a/zio-json/shared/src/test/scala/zio/json/Gens.scala +++ b/zio-json/shared/src/test/scala/zio/json/Gens.scala @@ -9,15 +9,15 @@ import scala.util.Try object Gens { val genBigInteger = Gen - .bigInt((BigInt(2).pow(128) - 1) * -1, BigInt(2).pow(128) - 1) + .bigInt((BigInt(2).pow(256) - 1) * -1, BigInt(2).pow(256) - 1) .map(_.bigInteger) - .filter(_.bitLength < 128) + .filter(_.bitLength < 256) val genBigDecimal = Gen - .bigDecimal((BigDecimal(2).pow(128) - 1) * -1, BigDecimal(2).pow(128) - 1) + .bigDecimal((BigDecimal(2).pow(256) - 1) * -1, BigDecimal(2).pow(256) - 1) .map(_.bigDecimal) - .filter(_.unscaledValue.bitLength < 128) + .filter(_.unscaledValue.bitLength < 256) val genUsAsciiString = Gen.string(Gen.oneOf(Gen.char('!', '~'))) diff --git a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala index 149668481..078126fa8 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala @@ -114,7 +114,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(Gen.long)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i.toDouble)))) }, test("valid (from BigDecimal)") { - check(genBigDecimal)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i.doubleValue())))) + check(genBigDecimal)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i.doubleValue)))) }, test("invalid edge cases") { val inputs = List( @@ -277,13 +277,18 @@ object SafeNumbersSpec extends ZIOSpecDefault { } }, test("valid (from BigDecimal)") { - check(genBigDecimal)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.floatValue())))) + check(genBigDecimal)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.floatValue)))) }, test("invalid float (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.float(s))(equalTo(FloatNone))) } ), suite("Int")( + test("valid edge cases") { + val input = List("00", "01", "0000001", "-2147483648", "2147483647") + + check(Gen.fromIterable(input))(x => assert(SafeNumbers.int(x))(equalTo(IntSome(x.toInt)))) + }, test("valid") { check(Gen.int)(d => assert(SafeNumbers.int(d.toString))(equalTo(IntSome(d)))) }, @@ -311,7 +316,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { ), suite("Long")( test("valid edge cases") { - val input = List("00", "01", "0000001", "-9223372036854775807", "9223372036854775806") + val input = List("00", "01", "0000001", "-9223372036854775808", "9223372036854775807") check(Gen.fromIterable(input))(x => assert(SafeNumbers.long(x))(equalTo(LongSome(x.toLong)))) }, From 4a298ec6475eb8aec2fe1fd503e384ae03c5ff36 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 11 Feb 2025 16:34:27 +0100 Subject: [PATCH 155/311] Fix returning of wrong value or throwing of exceptions during decoding of floating point numbers with extreme exponents (#1303) --- .../zio/json/internal/UnsafeNumbers.scala | 36 ++++++---- .../zio/json/internal/UnsafeNumbers.scala | 36 ++++++---- .../zio/json/internal/UnsafeNumbers.scala | 36 ++++++---- .../src/test/scala/zio/json/DecoderSpec.scala | 17 +++++ .../zio/json/internal/SafeNumbersSpec.scala | 69 +++++++++++-------- 5 files changed, 127 insertions(+), 67 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 70bb1b778..d4a3d6268 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -195,7 +195,6 @@ object UnsafeNumbers { } } } - if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -213,15 +212,20 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (negateExp) e10 += exp - else if (exp != -2147483648) e10 -= exp + if (negateExp) { + e10 += exp + if (e10 > 0) throw UnsafeNumber + } else if (exp != -2147483648) e10 -= exp else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { + if (loDigits == 0) throw UnsafeNumber if (negate) loM10 = -loM10 return java.math.BigDecimal.valueOf(loM10, -e10) } + val scale = loDigits + e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } @@ -235,12 +239,10 @@ object UnsafeNumbers { ): java.math.BigDecimal = { var loM10 = lo if (negate) loM10 = -loM10 - val bd = - if (loDigits != 0) java.math.BigDecimal.valueOf(loM10, -e10) - else java.math.BigDecimal.ZERO + val bd = java.math.BigDecimal.valueOf(loM10, -e10) if (hi eq null) return bd - var hiM10 = hi val scale = loDigits + e10 + var hiM10 = hi if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) hiM10 = hiM10.add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber @@ -300,7 +302,6 @@ object UnsafeNumbers { } } } - if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -318,12 +319,15 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (negateExp) e10 += exp - else if (exp != -2147483648) e10 -= exp + if (negateExp) { + e10 += exp + if (e10 > 0) throw UnsafeNumber + } else if (exp != -2147483648) e10 -= exp else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { + if (loDigits == 0) throw UnsafeNumber var x = if (e10 == 0) loM10.toFloat else { @@ -336,6 +340,8 @@ object UnsafeNumbers { if (negate) x = -x return x } + val scale = loDigits + e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } @@ -425,7 +431,6 @@ object UnsafeNumbers { } } } - if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -443,12 +448,15 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (negateExp) e10 += exp - else if (exp != -2147483648) e10 -= exp + if (negateExp) { + e10 += exp + if (e10 > 0) throw UnsafeNumber + } else if (exp != -2147483648) e10 -= exp else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { + if (loDigits == 0) throw UnsafeNumber var x = if (e10 == 0) loM10.toDouble else { @@ -465,6 +473,8 @@ object UnsafeNumbers { if (negate) x = -x return x } + val scale = loDigits + e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index ce269ae7a..3f263624c 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -195,7 +195,6 @@ object UnsafeNumbers { } } } - if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -213,15 +212,20 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (negateExp) e10 += exp - else if (exp != -2147483648) e10 -= exp + if (negateExp) { + e10 += exp + if (e10 > 0) throw UnsafeNumber + } else if (exp != -2147483648) e10 -= exp else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { + if (loDigits == 0) throw UnsafeNumber if (negate) loM10 = -loM10 return java.math.BigDecimal.valueOf(loM10, -e10) } + val scale = loDigits + e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } @@ -235,12 +239,10 @@ object UnsafeNumbers { ): java.math.BigDecimal = { var loM10 = lo if (negate) loM10 = -loM10 - val bd = - if (loDigits != 0) java.math.BigDecimal.valueOf(loM10, -e10) - else java.math.BigDecimal.ZERO + val bd = java.math.BigDecimal.valueOf(loM10, -e10) if (hi eq null) return bd - var hiM10 = hi val scale = loDigits + e10 + var hiM10 = hi if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) hiM10 = hiM10.add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber @@ -300,7 +302,6 @@ object UnsafeNumbers { } } } - if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -318,12 +319,15 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (negateExp) e10 += exp - else if (exp != -2147483648) e10 -= exp + if (negateExp) { + e10 += exp + if (e10 > 0) throw UnsafeNumber + } else if (exp != -2147483648) e10 -= exp else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { + if (loDigits == 0) throw UnsafeNumber var x = if (e10 == 0) loM10.toFloat else { @@ -336,6 +340,8 @@ object UnsafeNumbers { if (negate) x = -x return x } + val scale = loDigits + e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } @@ -425,7 +431,6 @@ object UnsafeNumbers { } } } - if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -443,12 +448,15 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (negateExp) e10 += exp - else if (exp != -2147483648) e10 -= exp + if (negateExp) { + e10 += exp + if (e10 > 0) throw UnsafeNumber + } else if (exp != -2147483648) e10 -= exp else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { + if (loDigits == 0) throw UnsafeNumber var x = if (e10 == 0) loM10.toDouble else { @@ -465,6 +473,8 @@ object UnsafeNumbers { if (negate) x = -x return x } + val scale = loDigits + e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index ce269ae7a..3f263624c 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -195,7 +195,6 @@ object UnsafeNumbers { } } } - if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -213,15 +212,20 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (negateExp) e10 += exp - else if (exp != -2147483648) e10 -= exp + if (negateExp) { + e10 += exp + if (e10 > 0) throw UnsafeNumber + } else if (exp != -2147483648) e10 -= exp else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { + if (loDigits == 0) throw UnsafeNumber if (negate) loM10 = -loM10 return java.math.BigDecimal.valueOf(loM10, -e10) } + val scale = loDigits + e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } @@ -235,12 +239,10 @@ object UnsafeNumbers { ): java.math.BigDecimal = { var loM10 = lo if (negate) loM10 = -loM10 - val bd = - if (loDigits != 0) java.math.BigDecimal.valueOf(loM10, -e10) - else java.math.BigDecimal.ZERO + val bd = java.math.BigDecimal.valueOf(loM10, -e10) if (hi eq null) return bd - var hiM10 = hi val scale = loDigits + e10 + var hiM10 = hi if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) hiM10 = hiM10.add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber @@ -300,7 +302,6 @@ object UnsafeNumbers { } } } - if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -318,12 +319,15 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (negateExp) e10 += exp - else if (exp != -2147483648) e10 -= exp + if (negateExp) { + e10 += exp + if (e10 > 0) throw UnsafeNumber + } else if (exp != -2147483648) e10 -= exp else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { + if (loDigits == 0) throw UnsafeNumber var x = if (e10 == 0) loM10.toFloat else { @@ -336,6 +340,8 @@ object UnsafeNumbers { if (negate) x = -x return x } + val scale = loDigits + e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } @@ -425,7 +431,6 @@ object UnsafeNumbers { } } } - if ((hiM10 eq null) && loDigits == 0) throw UnsafeNumber if ((current | 0x20) == 'e') { current = in.readChar().toInt val negateExp = current == '-' @@ -443,12 +448,15 @@ object UnsafeNumbers { } ) throw UnsafeNumber } - if (negateExp) e10 += exp - else if (exp != -2147483648) e10 -= exp + if (negateExp) { + e10 += exp + if (e10 > 0) throw UnsafeNumber + } else if (exp != -2147483648) e10 -= exp else throw UnsafeNumber } if (consume && current != -1) throw UnsafeNumber if (hiM10 eq null) { + if (loDigits == 0) throw UnsafeNumber var x = if (e10 == 0) loM10.toDouble else { @@ -465,6 +473,8 @@ object UnsafeNumbers { if (negate) x = -x return x } + val scale = loDigits + e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 7fbbc4f1b..0754c72be 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -83,6 +83,9 @@ object DecoderSpec extends ZIOSpecDefault { assert("-1.234567e9".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && assert("1.234567e9".fromJson[Float])(isRight(equalTo(1.234567e9f))) && assert("\"-1.234567e9\"".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && + assert("-1.23456789012345678901e-2147483648".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && + assert("123456789012345678901e+2147483647".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && + assert("-123456789012345678901e+2147483647".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && assert("\"Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && assert("\"+Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && assert("\"-Infinity\"".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && @@ -92,6 +95,9 @@ object DecoderSpec extends ZIOSpecDefault { test("double") { assert("-1.23456789012345e9".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && assert("\"-1.23456789012345e9\"".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && + assert("-1.23456789012345678901e-2147483648".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && + assert("123456789012345678901e+2147483647".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && + assert("-123456789012345678901e+2147483647".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && assert("\"Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && assert("\"+Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && assert("\"-Infinity\"".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && @@ -120,6 +126,17 @@ object DecoderSpec extends ZIOSpecDefault { isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) ) }, + test("BigDecimal exponent too large") { + assert("1.23456789012345678901e-2147483648".fromJson[BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert("123456789012345678901e+2147483647".fromJson[BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert("-123456789012345678901e+2147483647".fromJson[BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) + }, test("BigInteger") { assert("170141183460469231731687303715884105728".fromJson[BigInteger])( isRight(equalTo(new BigInteger("170141183460469231731687303715884105728"))) diff --git a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala index 078126fa8..cf013d2f9 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala @@ -10,7 +10,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { suite("SafeNumbers")( suite("BigDecimal")( test("valid big decimals") { - check(genBigDecimal)(i => assert(SafeNumbers.bigDecimal(i.toString, 2048))(isSome(equalTo(i)))) + check(genBigDecimal)(x => assert(SafeNumbers.bigDecimal(x.toString))(isSome(equalTo(x)))) }, test("invalid big decimals") { val invalidBigDecimalEdgeCases = List( @@ -80,7 +80,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInteger(s))(isNone)) }, test("valid big Integer") { - check(genBigInteger)(i => assert(SafeNumbers.bigInteger(i.toString, 2048))(isSome(equalTo(i)))) + check(genBigInteger)(x => assert(SafeNumbers.bigInteger(x.toString, 2048))(isSome(equalTo(x)))) }, test("invalid BigInteger") { check(genAlphaLowerString)(s => assert(SafeNumbers.bigInteger(s))(isNone)) @@ -88,33 +88,35 @@ object SafeNumbersSpec extends ZIOSpecDefault { ), suite("Byte")( test("valid Byte") { - check(Gen.byte(Byte.MinValue, Byte.MaxValue)) { b => - assert(SafeNumbers.byte(b.toString))(equalTo(ByteSome(b))) + check(Gen.byte(Byte.MinValue, Byte.MaxValue)) { x => + val r = SafeNumbers.byte(x.toString) + assert(r)(equalTo(ByteSome(x))) && assert(r.isEmpty)(equalTo(false)) } }, test("invalid Byte (numbers)") { - check(Gen.int.filter(i => i < Byte.MinValue || i > Byte.MaxValue)) { b => - assert(SafeNumbers.byte(b.toString))(equalTo(ByteNone)) + check(Gen.int.filter(x => x < Byte.MinValue || x > Byte.MaxValue)) { x => + assert(SafeNumbers.byte(x.toString))(equalTo(ByteNone)) } }, test("invalid Byte (text)") { - check(genAlphaLowerString)(b => assert(SafeNumbers.byte(b.toString))(equalTo(ByteNone))) + check(genAlphaLowerString)(s => assert(SafeNumbers.byte(s).isEmpty)(equalTo(true))) } ), suite("Double")( test("valid") { - check(Gen.double.filterNot(_.isNaN)) { d => - assert(SafeNumbers.double(d.toString))(equalTo(DoubleSome(d))) + check(Gen.double.filterNot(_.isNaN)) { x => + val r = SafeNumbers.double(x.toString) + assert(r)(equalTo(DoubleSome(x))) && assert(r.isEmpty)(equalTo(false)) } }, test("valid (from Int)") { - check(Gen.int)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i.toDouble)))) + check(Gen.int)(x => assert(SafeNumbers.double(x.toString))(equalTo(DoubleSome(x.toDouble)))) }, test("valid (from Long)") { - check(Gen.long)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i.toDouble)))) + check(Gen.long)(x => assert(SafeNumbers.double(x.toString))(equalTo(DoubleSome(x.toDouble)))) }, test("valid (from BigDecimal)") { - check(genBigDecimal)(i => assert(SafeNumbers.double(i.toString))(equalTo(DoubleSome(i.doubleValue)))) + check(genBigDecimal)(x => assert(SafeNumbers.double(x.toString))(equalTo(DoubleSome(x.doubleValue)))) }, test("invalid edge cases") { val inputs = List( @@ -133,7 +135,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { "0." + "9" * 99 ) - check(Gen.fromIterable(inputs))(i => assert(SafeNumbers.double(i))(equalTo(DoubleNone))) + check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.double(s))(equalTo(DoubleNone))) }, test("valid edge cases") { val inputs = List( @@ -185,22 +187,25 @@ object SafeNumbersSpec extends ZIOSpecDefault { assert(SafeNumbers.double("-Infinity"))(not(equalTo(DoubleNone))) }, test("invalid doubles (text)") { - check(genAlphaLowerString)(s => assert(SafeNumbers.double(s))(equalTo(DoubleNone))) + check(genAlphaLowerString)(s => assert(SafeNumbers.double(s).isEmpty)(equalTo(true))) } ), suite("Float")( test("valid") { - check(Gen.float.filterNot(_.isNaN))(d => assert(SafeNumbers.float(d.toString))(equalTo(FloatSome(d)))) + check(Gen.float.filterNot(_.isNaN)) { x => + val r = SafeNumbers.float(x.toString) + assert(r)(equalTo(FloatSome(x))) && assert(r.isEmpty)(equalTo(false)) + } }, test("large mantissa") { // https://github.com/zio/zio-json/issues/221 assert(SafeNumbers.float("1.199999988079071"))(equalTo(FloatSome(1.1999999f))) } @@ jvmOnly, test("valid (from Int)") { - check(Gen.int)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.toFloat)))) + check(Gen.int)(x => assert(SafeNumbers.float(x.toString))(equalTo(FloatSome(x.toFloat)))) }, test("valid (from Long)") { - check(Gen.long)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.toFloat)))) + check(Gen.long)(x => assert(SafeNumbers.float(x.toString))(equalTo(FloatSome(x.toFloat)))) }, test("invalid edge cases") { val inputs = List( @@ -218,7 +223,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { "0." + "9" * 99 ) - check(Gen.fromIterable(inputs))(i => assert(SafeNumbers.float(i))(equalTo(FloatNone))) + check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.float(s))(equalTo(FloatNone))) }, test("valid edge cases") { val inputs = List( @@ -272,15 +277,15 @@ object SafeNumbersSpec extends ZIOSpecDefault { } }, test("valid (from Double)") { - check(Gen.double.filterNot(_.isNaN)) { d => - assert(SafeNumbers.float(d.toString))(equalTo(FloatSome(d.toFloat))) + check(Gen.double.filterNot(_.isNaN)) { x => + assert(SafeNumbers.float(x.toString))(equalTo(FloatSome(x.toFloat))) } }, test("valid (from BigDecimal)") { check(genBigDecimal)(i => assert(SafeNumbers.float(i.toString))(equalTo(FloatSome(i.floatValue)))) }, test("invalid float (text)") { - check(genAlphaLowerString)(s => assert(SafeNumbers.float(s))(equalTo(FloatNone))) + check(genAlphaLowerString)(s => assert(SafeNumbers.float(s).isEmpty)(equalTo(true))) } ), suite("Int")( @@ -290,7 +295,10 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(Gen.fromIterable(input))(x => assert(SafeNumbers.int(x))(equalTo(IntSome(x.toInt)))) }, test("valid") { - check(Gen.int)(d => assert(SafeNumbers.int(d.toString))(equalTo(IntSome(d)))) + check(Gen.int) { x => + val r = SafeNumbers.int(x.toString) + assert(r)(equalTo(IntSome(x))) && assert(r.isEmpty)(equalTo(false)) + } }, test("invalid (edge cases)") { val input = List( @@ -334,21 +342,26 @@ object SafeNumbersSpec extends ZIOSpecDefault { check(Gen.fromIterable(input))(x => assert(SafeNumbers.long(x))(equalTo(LongNone))) }, test("valid") { - check(Gen.long)(d => assert(SafeNumbers.long(d.toString))(equalTo(LongSome(d)))) + check(Gen.long) { x => + val r = SafeNumbers.long(x.toString) + assert(r)(equalTo(LongSome(x))) && assert(r.isEmpty)(equalTo(false)) + } }, test("invalid (out of range)") { - val outOfRange = genBigInteger - .filter(_.bitLength > 63) + val outOfRange = genBigInteger.filter(_.bitLength > 63) check(outOfRange)(x => assert(SafeNumbers.long(x.toString))(equalTo(LongNone))) }, test("invalid (text)") { - check(genAlphaLowerString)(s => assert(SafeNumbers.long(s))(equalTo(LongNone))) + check(genAlphaLowerString)(s => assert(SafeNumbers.long(s).isEmpty)(equalTo(true))) } ), suite("Short")( test("valid") { - check(Gen.short)(d => assert(SafeNumbers.short(d.toString))(equalTo(ShortSome(d)))) + check(Gen.short) { x => + val r = SafeNumbers.short(x.toString) + assert(r)(equalTo(ShortSome(x))) && assert(r.isEmpty)(equalTo(false)) + } }, test("invalid (out of range)") { check(Gen.int.filter(i => i < Short.MinValue || i > Short.MaxValue))(d => @@ -356,7 +369,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { ) }, test("invalid (text)") { - check(genAlphaLowerString)(s => assert(SafeNumbers.short(s))(equalTo(ShortNone))) + check(genAlphaLowerString)(s => assert(SafeNumbers.short(s).isEmpty)(equalTo(true))) } ) ) From 70fb6d2173bd372cffb9b4d87b68b2f5268eb8f7 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 11 Feb 2025 19:47:53 +0100 Subject: [PATCH 156/311] Fix decoding invalid JSON values as `NaN` and `Infinity` values of doubles and floats (#1304) --- .../zio/json/internal/UnsafeNumbers.scala | 21 ++++----- .../zio/json/internal/UnsafeNumbers.scala | 21 ++++----- .../zio/json/internal/UnsafeNumbers.scala | 21 ++++----- .../src/test/scala/zio/json/DecoderSpec.scala | 44 ++++++++++++++----- 4 files changed, 57 insertions(+), 50 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index d4a3d6268..96c51caa1 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -139,12 +139,9 @@ object UnsafeNumbers { } } if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (negate) loM10 = -loM10 - return java.math.BigInteger.valueOf(loM10) - } + if (negate) loM10 = -loM10 + if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) if (loDigits != 0) { - if (negate) loM10 = -loM10 hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber } @@ -224,8 +221,6 @@ object UnsafeNumbers { if (negate) loM10 = -loM10 return java.math.BigDecimal.valueOf(loM10, -e10) } - val scale = loDigits + e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } @@ -241,8 +236,12 @@ object UnsafeNumbers { if (negate) loM10 = -loM10 val bd = java.math.BigDecimal.valueOf(loM10, -e10) if (hi eq null) return bd - val scale = loDigits + e10 + var scale = loDigits var hiM10 = hi + if (e10 != 0) { + scale += e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber + } if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) hiM10 = hiM10.add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber @@ -340,8 +339,6 @@ object UnsafeNumbers { if (negate) x = -x return x } - val scale = loDigits + e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } @@ -473,8 +470,6 @@ object UnsafeNumbers { if (negate) x = -x return x } - val scale = loDigits + e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } @@ -519,7 +514,7 @@ object UnsafeNumbers { i += 1 } val current = in.read() // to be consistent read the terminator - if (consume && current != -1) throw UnsafeNumber + if (consume && current != -1 || !consume && current != '"') throw UnsafeNumber } // 64-bit unsigned multiplication was adopted from the great Hacker's Delight function diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 3f263624c..89341f8f4 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -139,12 +139,9 @@ object UnsafeNumbers { } } if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (negate) loM10 = -loM10 - return java.math.BigInteger.valueOf(loM10) - } + if (negate) loM10 = -loM10 + if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) if (loDigits != 0) { - if (negate) loM10 = -loM10 hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber } @@ -224,8 +221,6 @@ object UnsafeNumbers { if (negate) loM10 = -loM10 return java.math.BigDecimal.valueOf(loM10, -e10) } - val scale = loDigits + e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } @@ -241,8 +236,12 @@ object UnsafeNumbers { if (negate) loM10 = -loM10 val bd = java.math.BigDecimal.valueOf(loM10, -e10) if (hi eq null) return bd - val scale = loDigits + e10 + var scale = loDigits var hiM10 = hi + if (e10 != 0) { + scale += e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber + } if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) hiM10 = hiM10.add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber @@ -340,8 +339,6 @@ object UnsafeNumbers { if (negate) x = -x return x } - val scale = loDigits + e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } @@ -473,8 +470,6 @@ object UnsafeNumbers { if (negate) x = -x return x } - val scale = loDigits + e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } @@ -519,7 +514,7 @@ object UnsafeNumbers { i += 1 } val current = in.read() // to be consistent read the terminator - if (consume && current != -1) throw UnsafeNumber + if (consume && current != -1 || !consume && current != '"') throw UnsafeNumber } @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 3f263624c..89341f8f4 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -139,12 +139,9 @@ object UnsafeNumbers { } } if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (negate) loM10 = -loM10 - return java.math.BigInteger.valueOf(loM10) - } + if (negate) loM10 = -loM10 + if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) if (loDigits != 0) { - if (negate) loM10 = -loM10 hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber } @@ -224,8 +221,6 @@ object UnsafeNumbers { if (negate) loM10 = -loM10 return java.math.BigDecimal.valueOf(loM10, -e10) } - val scale = loDigits + e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } @@ -241,8 +236,12 @@ object UnsafeNumbers { if (negate) loM10 = -loM10 val bd = java.math.BigDecimal.valueOf(loM10, -e10) if (hi eq null) return bd - val scale = loDigits + e10 + var scale = loDigits var hiM10 = hi + if (e10 != 0) { + scale += e10 + if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber + } if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) hiM10 = hiM10.add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber @@ -340,8 +339,6 @@ object UnsafeNumbers { if (negate) x = -x return x } - val scale = loDigits + e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } @@ -473,8 +470,6 @@ object UnsafeNumbers { if (negate) x = -x return x } - val scale = loDigits + e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } @@ -519,7 +514,7 @@ object UnsafeNumbers { i += 1 } val current = in.read() // to be consistent read the terminator - if (consume && current != -1) throw UnsafeNumber + if (consume && current != -1 || !consume && current != '"') throw UnsafeNumber } @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 0754c72be..98d33f45d 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -80,28 +80,49 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue("\"NaN\"".fromJson[Long].isLeft) }, test("float") { - assert("-1.234567e9".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && assert("1.234567e9".fromJson[Float])(isRight(equalTo(1.234567e9f))) && + assert("-1.234567e9".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && assert("\"-1.234567e9\"".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && - assert("-1.23456789012345678901e-2147483648".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && + assert("8.3e38".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && + assert("-8.3e38".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && + assert("1.23456789012345678901e-2147483648".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && assert("123456789012345678901e+2147483647".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && - assert("-123456789012345678901e+2147483647".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && + assert("1234567890123456789.01e+2147483647".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && + assert("1.0e-2147483647".fromJson[Float])(isRight(equalTo(0.0f))) && + assert("-1.0e-2147483647".fromJson[Float])(isRight(equalTo(-0.0f))) && + assert("123456789012345678.901e+2147483647".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && + assert("-123456789012345678.901e+2147483647".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && assert("\"Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && assert("\"+Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && assert("\"-Infinity\"".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && assertTrue("\"NaN\"".fromJson[Float].isRight) && + assertTrue("Infinity".fromJson[Float].isLeft) && + assertTrue("+Infinity".fromJson[Float].isLeft) && + assertTrue("-Infinity".fromJson[Float].isLeft) && + assertTrue("NaN".fromJson[Float].isLeft) && assertTrue("+1.234567e9".fromJson[Float].isLeft) }, test("double") { + assert("1.23456789012345e9".fromJson[Double])(isRight(equalTo(1.23456789012345e9))) && assert("-1.23456789012345e9".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && assert("\"-1.23456789012345e9\"".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && - assert("-1.23456789012345678901e-2147483648".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && + assert("1.8e308".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && + assert("-1.8e308".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && + assert("1.23456789012345678901e-2147483648".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && + assert("1234567890123456789.01e+2147483647".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && assert("123456789012345678901e+2147483647".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && - assert("-123456789012345678901e+2147483647".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && + assert("1.0e-2147483647".fromJson[Double])(isRight(equalTo(0.0))) && + assert("-1.0e-2147483647".fromJson[Double])(isRight(equalTo(-0.0))) && + assert("123456789012345678.901e+2147483647".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && + assert("-123456789012345678.901e+2147483647".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && assert("\"Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && assert("\"+Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && assert("\"-Infinity\"".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && assertTrue("\"NaN\"".fromJson[Double].isRight) && + assertTrue("Infinity".fromJson[Double].isLeft) && + assertTrue("+Infinity".fromJson[Double].isLeft) && + assertTrue("-Infinity".fromJson[Double].isLeft) && + assertTrue("NaN".fromJson[Double].isLeft) && assertTrue("+1.23456789012345e9".fromJson[Double].isLeft) }, test("BigDecimal") { @@ -130,10 +151,10 @@ object DecoderSpec extends ZIOSpecDefault { assert("1.23456789012345678901e-2147483648".fromJson[BigDecimal])( isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) ) && - assert("123456789012345678901e+2147483647".fromJson[BigDecimal])( + assert("1234567890123456789.01e+2147483647".fromJson[BigDecimal])( isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) ) && - assert("-123456789012345678901e+2147483647".fromJson[BigDecimal])( + assert("123456789012345678901e+2147483647".fromJson[BigDecimal])( isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) ) }, @@ -150,13 +171,14 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue("\"NaN\"".fromJson[BigInteger].isLeft) }, test("BigInteger too large") { - // this big integer consumes more than 256 bits assert( "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" .fromJson[java.math.BigInteger] - )( - isLeft(equalTo("(expected a 256-bit BigInteger)")) - ) + )(isLeft(equalTo("(expected a 256-bit BigInteger)"))) && + assert( + "17014118346046923173168730371588410572848946516548466848651357486465481896465316846" + .fromJson[java.math.BigInteger] + )(isLeft(equalTo("(expected a 256-bit BigInteger)"))) }, test("collections") { val arr = """[1, 2, 3]""" From 8062803001326d84aec1720d683f6cdbcbad7d95 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 12 Feb 2025 10:06:41 +0100 Subject: [PATCH 157/311] More efficient decoding of big numbers (#1305) --- .../zio/json/internal/UnsafeNumbers.scala | 118 ++++++++++-------- .../zio/json/internal/UnsafeNumbers.scala | 118 ++++++++++-------- .../zio/json/internal/UnsafeNumbers.scala | 118 ++++++++++-------- .../src/test/scala/zio/json/DecoderSpec.scala | 14 +-- 4 files changed, 196 insertions(+), 172 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 96c51caa1..637b993d1 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -124,9 +124,10 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + } else { if (negate) loM10 = -loM10 val bd = java.math.BigDecimal.valueOf(loM10) if (hiM10 eq null) hiM10 = bd @@ -134,17 +135,15 @@ object UnsafeNumbers { hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber } - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } if (consume && current != -1) throw UnsafeNumber if (negate) loM10 = -loM10 if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) - if (loDigits != 0) { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber hiM10.unscaledValue } @@ -162,17 +161,18 @@ object UnsafeNumbers { var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { loM10 = (current - '0').toLong - loDigits += 1 + loDigits = 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } } @@ -182,13 +182,15 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') - loDigits += 1 - e10 -= 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + e10 -= 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 + e10 -= 1 } } } @@ -234,16 +236,16 @@ object UnsafeNumbers { ): java.math.BigDecimal = { var loM10 = lo if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10, -e10) - if (hi eq null) return bd - var scale = loDigits - var hiM10 = hi - if (e10 != 0) { - scale += e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber - } - if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) - hiM10 = hiM10.add(bd) + var hiM10 = java.math.BigDecimal.valueOf(loM10, -e10) + if (hi eq null) return hiM10 + val n = loDigits.toLong + e10 + if ( + n.toInt != n || { + val scale = hi.scale - n + scale.toInt != scale + } + ) throw UnsafeNumber + hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber hiM10 } @@ -271,17 +273,18 @@ object UnsafeNumbers { var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { loM10 = (current - '0').toLong - loDigits += 1 + loDigits = 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } } @@ -291,13 +294,15 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') - loDigits += 1 - e10 -= 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + e10 -= 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 + e10 -= 1 } } } @@ -398,17 +403,18 @@ object UnsafeNumbers { var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { loM10 = (current - '0').toLong - loDigits += 1 + loDigits = 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } } @@ -418,13 +424,15 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') - loDigits += 1 - e10 -= 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + e10 -= 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 + e10 -= 1 } } } diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 89341f8f4..18799c5a5 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -124,9 +124,10 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { if (negate) loM10 = -loM10 val bd = java.math.BigDecimal.valueOf(loM10) if (hiM10 eq null) hiM10 = bd @@ -134,17 +135,15 @@ object UnsafeNumbers { hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber } - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } if (consume && current != -1) throw UnsafeNumber if (negate) loM10 = -loM10 if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) - if (loDigits != 0) { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber hiM10.unscaledValue } @@ -162,17 +161,18 @@ object UnsafeNumbers { var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { loM10 = (current - '0').toLong - loDigits += 1 + loDigits = 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } } @@ -182,13 +182,15 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - e10 -= 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + e10 -= 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 + e10 -= 1 } } } @@ -234,16 +236,16 @@ object UnsafeNumbers { ): java.math.BigDecimal = { var loM10 = lo if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10, -e10) - if (hi eq null) return bd - var scale = loDigits - var hiM10 = hi - if (e10 != 0) { - scale += e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber - } - if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) - hiM10 = hiM10.add(bd) + var hiM10 = java.math.BigDecimal.valueOf(loM10, -e10) + if (hi eq null) return hiM10 + val n = loDigits.toLong + e10 + if ( + n.toInt != n || { + val scale = hi.scale - n + scale.toInt != scale + } + ) throw UnsafeNumber + hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber hiM10 } @@ -271,17 +273,18 @@ object UnsafeNumbers { var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { loM10 = (current - '0').toLong - loDigits += 1 + loDigits = 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } } @@ -291,13 +294,15 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - e10 -= 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + e10 -= 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 + e10 -= 1 } } } @@ -398,17 +403,18 @@ object UnsafeNumbers { var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { loM10 = (current - '0').toLong - loDigits += 1 + loDigits = 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } } @@ -418,13 +424,15 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - e10 -= 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + e10 -= 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 + e10 -= 1 } } } diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 89341f8f4..18799c5a5 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -124,9 +124,10 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { if (negate) loM10 = -loM10 val bd = java.math.BigDecimal.valueOf(loM10) if (hiM10 eq null) hiM10 = bd @@ -134,17 +135,15 @@ object UnsafeNumbers { hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber } - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } if (consume && current != -1) throw UnsafeNumber if (negate) loM10 = -loM10 if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) - if (loDigits != 0) { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber hiM10.unscaledValue } @@ -162,17 +161,18 @@ object UnsafeNumbers { var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { loM10 = (current - '0').toLong - loDigits += 1 + loDigits = 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } } @@ -182,13 +182,15 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - e10 -= 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + e10 -= 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 + e10 -= 1 } } } @@ -234,16 +236,16 @@ object UnsafeNumbers { ): java.math.BigDecimal = { var loM10 = lo if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10, -e10) - if (hi eq null) return bd - var scale = loDigits - var hiM10 = hi - if (e10 != 0) { - scale += e10 - if (((loDigits ^ scale) & (e10 ^ scale)) < 0) throw UnsafeNumber - } - if (scale != 0) hiM10 = hiM10.scaleByPowerOfTen(scale) - hiM10 = hiM10.add(bd) + var hiM10 = java.math.BigDecimal.valueOf(loM10, -e10) + if (hi eq null) return hiM10 + val n = loDigits.toLong + e10 + if ( + n.toInt != n || { + val scale = hi.scale - n + scale.toInt != scale + } + ) throw UnsafeNumber + hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber hiM10 } @@ -271,17 +273,18 @@ object UnsafeNumbers { var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { loM10 = (current - '0').toLong - loDigits += 1 + loDigits = 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } } @@ -291,13 +294,15 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - e10 -= 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + e10 -= 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 + e10 -= 1 } } } @@ -398,17 +403,18 @@ object UnsafeNumbers { var hiM10: java.math.BigDecimal = null if ('0' <= current && current <= '9') { loM10 = (current - '0').toLong - loDigits += 1 + loDigits = 1 while ({ current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 } } } @@ -418,13 +424,15 @@ object UnsafeNumbers { current = in.read() '0' <= current && current <= '9' }) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - e10 -= 1 - if (loM10 >= 100000000000000000L) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + e10 -= 1 + } else { hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = 0 - loDigits = 0 + loM10 = (current - '0').toLong + loDigits = 1 + e10 -= 1 } } } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 98d33f45d..014568683 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -87,11 +87,11 @@ object DecoderSpec extends ZIOSpecDefault { assert("-8.3e38".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && assert("1.23456789012345678901e-2147483648".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && assert("123456789012345678901e+2147483647".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && - assert("1234567890123456789.01e+2147483647".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && + assert("12345678901234567890.1e+2147483647".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && assert("1.0e-2147483647".fromJson[Float])(isRight(equalTo(0.0f))) && assert("-1.0e-2147483647".fromJson[Float])(isRight(equalTo(-0.0f))) && - assert("123456789012345678.901e+2147483647".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && - assert("-123456789012345678.901e+2147483647".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && + assert("1234567890123456789.01e+2147483647".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && + assert("-1234567890123456789.01e+2147483647".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && assert("\"Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && assert("\"+Infinity\"".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && assert("\"-Infinity\"".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && @@ -109,12 +109,12 @@ object DecoderSpec extends ZIOSpecDefault { assert("1.8e308".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && assert("-1.8e308".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && assert("1.23456789012345678901e-2147483648".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && - assert("1234567890123456789.01e+2147483647".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && + assert("12345678901234567890.1e+2147483647".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && assert("123456789012345678901e+2147483647".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && assert("1.0e-2147483647".fromJson[Double])(isRight(equalTo(0.0))) && assert("-1.0e-2147483647".fromJson[Double])(isRight(equalTo(-0.0))) && - assert("123456789012345678.901e+2147483647".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && - assert("-123456789012345678.901e+2147483647".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && + assert("1234567890123456789.01e+2147483647".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && + assert("-1234567890123456789.01e+2147483647".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && assert("\"Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && assert("\"+Infinity\"".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && assert("\"-Infinity\"".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && @@ -151,7 +151,7 @@ object DecoderSpec extends ZIOSpecDefault { assert("1.23456789012345678901e-2147483648".fromJson[BigDecimal])( isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) ) && - assert("1234567890123456789.01e+2147483647".fromJson[BigDecimal])( + assert("12345678901234567890.1e+2147483647".fromJson[BigDecimal])( isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) ) && assert("123456789012345678901e+2147483647".fromJson[BigDecimal])( From 099b3cfb51d263be255f423a7bf354bab757ff06 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 12 Feb 2025 21:54:03 +0100 Subject: [PATCH 158/311] More efficient number parsing (#1309) --- .../zio/json/internal/UnsafeNumbers.scala | 398 ++++++++++-------- .../zio/json/internal/UnsafeNumbers.scala | 398 ++++++++++-------- .../zio/json/internal/UnsafeNumbers.scala | 398 ++++++++++-------- 3 files changed, 654 insertions(+), 540 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 637b993d1..910b2c51e 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -60,23 +60,25 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var accum = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - accum < -214748364 || { - accum = accum * 10 + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber + if (current >= '0' && current <= '9') { + var accum = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + accum < -214748364 || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return accum + else if (accum != -2147483648) return -accum + } } - if (consume && current != -1) throw UnsafeNumber - if (negate) accum - else if (accum != -2147483648) -accum - else throw UnsafeNumber + throw UnsafeNumber } def long(num: String): Long = @@ -88,23 +90,25 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var accum = ('0' - current).toLong - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - accum < -922337203685477580L || { - accum = (accum << 3) + (accum << 1) + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber + if (current >= '0' && current <= '9') { + var accum = ('0' - current).toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -922337203685477580L || { + accum = (accum << 3) + (accum << 1) + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return accum + else if (accum != -9223372036854775808L) return -accum + } } - if (consume && current != -1) throw UnsafeNumber - if (negate) accum - else if (accum != -9223372036854775808L) -accum - else throw UnsafeNumber + throw UnsafeNumber } def bigInteger(num: String, max_bits: Int): java.math.BigInteger = @@ -116,35 +120,37 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var loM10 = (current - '0').toLong - var loDigits = 1 - var hiM10: java.math.BigDecimal = null - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') - loDigits += 1 - } else { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + if (current >= '0' && current <= '9') { + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if (loM10 < 922337203685477580L) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + } else { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = (current - '0').toLong + loDigits = 1 } - loM10 = (current - '0').toLong - loDigits = 1 + } + if (!consume || current == -1) { + if (negate) loM10 = -loM10 + if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) + val bi = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)).unscaledValue + if (bi.bitLength < max_bits) return bi } } - if (consume && current != -1) throw UnsafeNumber - if (negate) loM10 = -loM10 - if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - hiM10.unscaledValue + throw UnsafeNumber } def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = @@ -159,18 +165,18 @@ object UnsafeNumbers { var loM10 = 0L var loDigits = 0 var hiM10: java.math.BigDecimal = null - if ('0' <= current && current <= '9') { + if (current >= '0' && current <= '9') { loM10 = (current - '0').toLong loDigits = 1 while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') loDigits += 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 } @@ -180,50 +186,71 @@ object UnsafeNumbers { if (current == '.') { while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') loDigits += 1 e10 -= 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 e10 -= 1 } } } - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 + if ( + loDigits != 0 && ((current | 0x20) != 'e' || { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + (current >= '0' && current <= '9') && { + var exp = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber } - ) throw UnsafeNumber + negateExp && { + e10 += exp + e10 <= 0 + } || !negateExp && { + e10 -= exp + exp != -2147483648 + } + } + }) && (!consume || current == -1) + ) { + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return java.math.BigDecimal.valueOf(loM10, -e10) } - if (negateExp) { - e10 += exp - if (e10 > 0) throw UnsafeNumber - } else if (exp != -2147483648) e10 -= exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (loDigits == 0) throw UnsafeNumber - if (negate) loM10 = -loM10 - return java.math.BigDecimal.valueOf(loM10, -e10) + return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) + throw UnsafeNumber + } + + @noinline private[this] def toBigDecimal( + hi: java.math.BigDecimal, + lo: Long, + loDigits: Int, + max_bits: Int, + negate: Boolean + ): java.math.BigDecimal = { + var loM10 = lo + if (negate) loM10 = -loM10 + var hiM10 = java.math.BigDecimal.valueOf(loM10) + if (hi eq null) return hiM10 + hiM10 = hi.scaleByPowerOfTen(loDigits).add(hiM10) + if (hiM10.unscaledValue.bitLength < max_bits) return hiM10 + throw UnsafeNumber } @noinline private[this] def toBigDecimal( @@ -240,14 +267,15 @@ object UnsafeNumbers { if (hi eq null) return hiM10 val n = loDigits.toLong + e10 if ( - n.toInt != n || { + n.toInt == n && { val scale = hi.scale - n - scale.toInt != scale + scale.toInt == scale + } && { + hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) + hiM10.unscaledValue.bitLength < max_bits } - ) throw UnsafeNumber - hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - hiM10 + ) return hiM10 + throw UnsafeNumber } def float(num: String, max_bits: Int): Float = @@ -271,18 +299,18 @@ object UnsafeNumbers { var loM10 = 0L var loDigits = 0 var hiM10: java.math.BigDecimal = null - if ('0' <= current && current <= '9') { + if (current >= '0' && current <= '9') { loM10 = (current - '0').toLong loDigits = 1 while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') loDigits += 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 } @@ -292,59 +320,64 @@ object UnsafeNumbers { if (current == '.') { while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') loDigits += 1 e10 -= 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 e10 -= 1 } } } - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 + if ( + loDigits != 0 && ((current | 0x20) != 'e' || { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + (current >= '0' && current <= '9') && { + var exp = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + negateExp && { + e10 += exp + e10 <= 0 + } || !negateExp && { + e10 -= exp + exp != -2147483648 } - ) throw UnsafeNumber - } - if (negateExp) { - e10 += exp - if (e10 > 0) throw UnsafeNumber - } else if (exp != -2147483648) e10 -= exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (loDigits == 0) throw UnsafeNumber - var x = - if (e10 == 0) loM10.toFloat - else { - if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { - val pow10 = pow10Doubles - (if (e10 < 0) loM10 / pow10(-e10) - else loM10 * pow10(e10)).toFloat - } else toFloat(loM10, e10) } - if (negate) x = -x - return x + }) && (!consume || current == -1) + ) { + if (hiM10 eq null) { + var x = + if (e10 == 0) loM10.toFloat + else { + if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { + val pow10 = pow10Doubles + (if (e10 < 0) loM10 / pow10(-e10) + else loM10 * pow10(e10)).toFloat + } else toFloat(loM10, e10) + } + if (negate) x = -x + return x + } + return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue + throw UnsafeNumber } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -401,12 +434,12 @@ object UnsafeNumbers { var loM10 = 0L var loDigits = 0 var hiM10: java.math.BigDecimal = null - if ('0' <= current && current <= '9') { + if (current >= '0' && current <= '9') { loM10 = (current - '0').toLong loDigits = 1 while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') @@ -422,7 +455,7 @@ object UnsafeNumbers { if (current == '.') { while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') @@ -436,49 +469,54 @@ object UnsafeNumbers { } } } - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 + if ( + loDigits != 0 && ((current | 0x20) != 'e' || { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + (current >= '0' && current <= '9') && { + var exp = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + negateExp && { + e10 += exp + e10 <= 0 + } || !negateExp && { + e10 -= exp + exp != -2147483648 } - ) throw UnsafeNumber - } - if (negateExp) { - e10 += exp - if (e10 > 0) throw UnsafeNumber - } else if (exp != -2147483648) e10 -= exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (loDigits == 0) throw UnsafeNumber - var x = - if (e10 == 0) loM10.toDouble - else { - if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { - val pow10 = pow10Doubles - if (e10 < 0) loM10 / pow10(-e10) - else if (e10 <= 22) loM10 * pow10(e10) - else { - val slop = 16 - loDigits - (loM10 * pow10(slop)) * pow10(e10 - slop) - } - } else toDouble(loM10, e10) } - if (negate) x = -x - return x + }) && (!consume || current == -1) + ) { + if (hiM10 eq null) { + var x = + if (e10 == 0) loM10.toDouble + else { + if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { + val pow10 = pow10Doubles + if (e10 < 0) loM10 / pow10(-e10) + else if (e10 <= 22) loM10 * pow10(e10) + else { + val slop = 16 - loDigits + (loM10 * pow10(slop)) * pow10(e10 - slop) + } + } else toDouble(loM10, e10) + } + if (negate) x = -x + return x + } + return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue + throw UnsafeNumber } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 18799c5a5..218cc2357 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -60,23 +60,25 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var accum = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - accum < -214748364 || { - accum = accum * 10 + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber + if (current >= '0' && current <= '9') { + var accum = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + accum < -214748364 || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return accum + else if (accum != -2147483648) return -accum + } } - if (consume && current != -1) throw UnsafeNumber - if (negate) accum - else if (accum != -2147483648) -accum - else throw UnsafeNumber + throw UnsafeNumber } def long(num: String): Long = @@ -88,23 +90,25 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var accum = ('0' - current).toLong - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - accum < -922337203685477580L || { - accum = accum * 10 + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber + if (current >= '0' && current <= '9') { + var accum = ('0' - current).toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -922337203685477580L || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return accum + else if (accum != -9223372036854775808L) return -accum + } } - if (consume && current != -1) throw UnsafeNumber - if (negate) accum - else if (accum != -9223372036854775808L) -accum - else throw UnsafeNumber + throw UnsafeNumber } def bigInteger(num: String, max_bits: Int): java.math.BigInteger = @@ -116,35 +120,37 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var loM10 = (current - '0').toLong - var loDigits = 1 - var hiM10: java.math.BigDecimal = null - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - } else { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + if (current >= '0' && current <= '9') { + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = (current - '0').toLong + loDigits = 1 } - loM10 = (current - '0').toLong - loDigits = 1 + } + if (!consume || current == -1) { + if (negate) loM10 = -loM10 + if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) + val bi = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)).unscaledValue + if (bi.bitLength < max_bits) return bi } } - if (consume && current != -1) throw UnsafeNumber - if (negate) loM10 = -loM10 - if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - hiM10.unscaledValue + throw UnsafeNumber } def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = @@ -159,18 +165,18 @@ object UnsafeNumbers { var loM10 = 0L var loDigits = 0 var hiM10: java.math.BigDecimal = null - if ('0' <= current && current <= '9') { + if (current >= '0' && current <= '9') { loM10 = (current - '0').toLong loDigits = 1 while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') loDigits += 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 } @@ -180,50 +186,71 @@ object UnsafeNumbers { if (current == '.') { while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') loDigits += 1 e10 -= 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 e10 -= 1 } } } - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 + if ( + loDigits != 0 && ((current | 0x20) != 'e' || { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + (current >= '0' && current <= '9') && { + var exp = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber } - ) throw UnsafeNumber + negateExp && { + e10 += exp + e10 <= 0 + } || !negateExp && { + e10 -= exp + exp != -2147483648 + } + } + }) && (!consume || current == -1) + ) { + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return java.math.BigDecimal.valueOf(loM10, -e10) } - if (negateExp) { - e10 += exp - if (e10 > 0) throw UnsafeNumber - } else if (exp != -2147483648) e10 -= exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (loDigits == 0) throw UnsafeNumber - if (negate) loM10 = -loM10 - return java.math.BigDecimal.valueOf(loM10, -e10) + return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) + throw UnsafeNumber + } + + @noinline private[this] def toBigDecimal( + hi: java.math.BigDecimal, + lo: Long, + loDigits: Int, + max_bits: Int, + negate: Boolean + ): java.math.BigDecimal = { + var loM10 = lo + if (negate) loM10 = -loM10 + var hiM10 = java.math.BigDecimal.valueOf(loM10) + if (hi eq null) return hiM10 + hiM10 = hi.scaleByPowerOfTen(loDigits).add(hiM10) + if (hiM10.unscaledValue.bitLength < max_bits) return hiM10 + throw UnsafeNumber } @noinline private[this] def toBigDecimal( @@ -240,14 +267,15 @@ object UnsafeNumbers { if (hi eq null) return hiM10 val n = loDigits.toLong + e10 if ( - n.toInt != n || { + n.toInt == n && { val scale = hi.scale - n - scale.toInt != scale + scale.toInt == scale + } && { + hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) + hiM10.unscaledValue.bitLength < max_bits } - ) throw UnsafeNumber - hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - hiM10 + ) return hiM10 + throw UnsafeNumber } def float(num: String, max_bits: Int): Float = @@ -271,18 +299,18 @@ object UnsafeNumbers { var loM10 = 0L var loDigits = 0 var hiM10: java.math.BigDecimal = null - if ('0' <= current && current <= '9') { + if (current >= '0' && current <= '9') { loM10 = (current - '0').toLong loDigits = 1 while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') loDigits += 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 } @@ -292,59 +320,64 @@ object UnsafeNumbers { if (current == '.') { while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') loDigits += 1 e10 -= 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 e10 -= 1 } } } - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 + if ( + loDigits != 0 && ((current | 0x20) != 'e' || { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + (current >= '0' && current <= '9') && { + var exp = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + negateExp && { + e10 += exp + e10 <= 0 + } || !negateExp && { + e10 -= exp + exp != -2147483648 } - ) throw UnsafeNumber - } - if (negateExp) { - e10 += exp - if (e10 > 0) throw UnsafeNumber - } else if (exp != -2147483648) e10 -= exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (loDigits == 0) throw UnsafeNumber - var x = - if (e10 == 0) loM10.toFloat - else { - if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { - val pow10 = pow10Doubles - (if (e10 < 0) loM10 / pow10(-e10) - else loM10 * pow10(e10)).toFloat - } else toFloat(loM10, e10) } - if (negate) x = -x - return x + }) && (!consume || current == -1) + ) { + if (hiM10 eq null) { + var x = + if (e10 == 0) loM10.toFloat + else { + if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { + val pow10 = pow10Doubles + (if (e10 < 0) loM10 / pow10(-e10) + else loM10 * pow10(e10)).toFloat + } else toFloat(loM10, e10) + } + if (negate) x = -x + return x + } + return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue + throw UnsafeNumber } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -401,12 +434,12 @@ object UnsafeNumbers { var loM10 = 0L var loDigits = 0 var hiM10: java.math.BigDecimal = null - if ('0' <= current && current <= '9') { + if (current >= '0' && current <= '9') { loM10 = (current - '0').toLong loDigits = 1 while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') @@ -422,7 +455,7 @@ object UnsafeNumbers { if (current == '.') { while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') @@ -436,49 +469,54 @@ object UnsafeNumbers { } } } - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 + if ( + loDigits != 0 && ((current | 0x20) != 'e' || { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + (current >= '0' && current <= '9') && { + var exp = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + negateExp && { + e10 += exp + e10 <= 0 + } || !negateExp && { + e10 -= exp + exp != -2147483648 } - ) throw UnsafeNumber - } - if (negateExp) { - e10 += exp - if (e10 > 0) throw UnsafeNumber - } else if (exp != -2147483648) e10 -= exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (loDigits == 0) throw UnsafeNumber - var x = - if (e10 == 0) loM10.toDouble - else { - if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { - val pow10 = pow10Doubles - if (e10 < 0) loM10 / pow10(-e10) - else if (e10 <= 22) loM10 * pow10(e10) - else { - val slop = 16 - loDigits - (loM10 * pow10(slop)) * pow10(e10 - slop) - } - } else toDouble(loM10, e10) } - if (negate) x = -x - return x + }) && (!consume || current == -1) + ) { + if (hiM10 eq null) { + var x = + if (e10 == 0) loM10.toDouble + else { + if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { + val pow10 = pow10Doubles + if (e10 < 0) loM10 / pow10(-e10) + else if (e10 <= 22) loM10 * pow10(e10) + else { + val slop = 16 - loDigits + (loM10 * pow10(slop)) * pow10(e10 - slop) + } + } else toDouble(loM10, e10) + } + if (negate) x = -x + return x + } + return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue + throw UnsafeNumber } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 18799c5a5..218cc2357 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -60,23 +60,25 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var accum = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - accum < -214748364 || { - accum = accum * 10 + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber + if (current >= '0' && current <= '9') { + var accum = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + accum < -214748364 || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return accum + else if (accum != -2147483648) return -accum + } } - if (consume && current != -1) throw UnsafeNumber - if (negate) accum - else if (accum != -2147483648) -accum - else throw UnsafeNumber + throw UnsafeNumber } def long(num: String): Long = @@ -88,23 +90,25 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var accum = ('0' - current).toLong - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - accum < -922337203685477580L || { - accum = accum * 10 + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber + if (current >= '0' && current <= '9') { + var accum = ('0' - current).toLong + while ({ + current = in.read() + '0' <= current && current <= '9' + }) { + if ( + accum < -922337203685477580L || { + accum = accum * 10 + ('0' - current) + accum > 0 + } + ) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return accum + else if (accum != -9223372036854775808L) return -accum + } } - if (consume && current != -1) throw UnsafeNumber - if (negate) accum - else if (accum != -9223372036854775808L) -accum - else throw UnsafeNumber + throw UnsafeNumber } def bigInteger(num: String, max_bits: Int): java.math.BigInteger = @@ -116,35 +120,37 @@ object UnsafeNumbers { else in.nextNonWhitespace().toInt val negate = current == '-' if (negate) current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var loM10 = (current - '0').toLong - var loDigits = 1 - var hiM10: java.math.BigDecimal = null - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - } else { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + if (current >= '0' && current <= '9') { + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = (current - '0').toLong + loDigits = 1 } - loM10 = (current - '0').toLong - loDigits = 1 + } + if (!consume || current == -1) { + if (negate) loM10 = -loM10 + if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) + val bi = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)).unscaledValue + if (bi.bitLength < max_bits) return bi } } - if (consume && current != -1) throw UnsafeNumber - if (negate) loM10 = -loM10 - if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - hiM10.unscaledValue + throw UnsafeNumber } def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = @@ -159,18 +165,18 @@ object UnsafeNumbers { var loM10 = 0L var loDigits = 0 var hiM10: java.math.BigDecimal = null - if ('0' <= current && current <= '9') { + if (current >= '0' && current <= '9') { loM10 = (current - '0').toLong loDigits = 1 while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') loDigits += 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 } @@ -180,50 +186,71 @@ object UnsafeNumbers { if (current == '.') { while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') loDigits += 1 e10 -= 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 e10 -= 1 } } } - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 + if ( + loDigits != 0 && ((current | 0x20) != 'e' || { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + (current >= '0' && current <= '9') && { + var exp = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber } - ) throw UnsafeNumber + negateExp && { + e10 += exp + e10 <= 0 + } || !negateExp && { + e10 -= exp + exp != -2147483648 + } + } + }) && (!consume || current == -1) + ) { + if (hiM10 eq null) { + if (negate) loM10 = -loM10 + return java.math.BigDecimal.valueOf(loM10, -e10) } - if (negateExp) { - e10 += exp - if (e10 > 0) throw UnsafeNumber - } else if (exp != -2147483648) e10 -= exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (loDigits == 0) throw UnsafeNumber - if (negate) loM10 = -loM10 - return java.math.BigDecimal.valueOf(loM10, -e10) + return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) + throw UnsafeNumber + } + + @noinline private[this] def toBigDecimal( + hi: java.math.BigDecimal, + lo: Long, + loDigits: Int, + max_bits: Int, + negate: Boolean + ): java.math.BigDecimal = { + var loM10 = lo + if (negate) loM10 = -loM10 + var hiM10 = java.math.BigDecimal.valueOf(loM10) + if (hi eq null) return hiM10 + hiM10 = hi.scaleByPowerOfTen(loDigits).add(hiM10) + if (hiM10.unscaledValue.bitLength < max_bits) return hiM10 + throw UnsafeNumber } @noinline private[this] def toBigDecimal( @@ -240,14 +267,15 @@ object UnsafeNumbers { if (hi eq null) return hiM10 val n = loDigits.toLong + e10 if ( - n.toInt != n || { + n.toInt == n && { val scale = hi.scale - n - scale.toInt != scale + scale.toInt == scale + } && { + hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) + hiM10.unscaledValue.bitLength < max_bits } - ) throw UnsafeNumber - hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - hiM10 + ) return hiM10 + throw UnsafeNumber } def float(num: String, max_bits: Int): Float = @@ -271,18 +299,18 @@ object UnsafeNumbers { var loM10 = 0L var loDigits = 0 var hiM10: java.math.BigDecimal = null - if ('0' <= current && current <= '9') { + if (current >= '0' && current <= '9') { loM10 = (current - '0').toLong loDigits = 1 while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') loDigits += 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 } @@ -292,59 +320,64 @@ object UnsafeNumbers { if (current == '.') { while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') loDigits += 1 e10 -= 1 } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) + hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) loM10 = (current - '0').toLong loDigits = 1 e10 -= 1 } } } - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 + if ( + loDigits != 0 && ((current | 0x20) != 'e' || { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + (current >= '0' && current <= '9') && { + var exp = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + negateExp && { + e10 += exp + e10 <= 0 + } || !negateExp && { + e10 -= exp + exp != -2147483648 } - ) throw UnsafeNumber - } - if (negateExp) { - e10 += exp - if (e10 > 0) throw UnsafeNumber - } else if (exp != -2147483648) e10 -= exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (loDigits == 0) throw UnsafeNumber - var x = - if (e10 == 0) loM10.toFloat - else { - if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { - val pow10 = pow10Doubles - (if (e10 < 0) loM10 / pow10(-e10) - else loM10 * pow10(e10)).toFloat - } else toFloat(loM10, e10) } - if (negate) x = -x - return x + }) && (!consume || current == -1) + ) { + if (hiM10 eq null) { + var x = + if (e10 == 0) loM10.toFloat + else { + if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { + val pow10 = pow10Doubles + (if (e10 < 0) loM10 / pow10(-e10) + else loM10 * pow10(e10)).toFloat + } else toFloat(loM10, e10) + } + if (negate) x = -x + return x + } + return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue + throw UnsafeNumber } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical @@ -401,12 +434,12 @@ object UnsafeNumbers { var loM10 = 0L var loDigits = 0 var hiM10: java.math.BigDecimal = null - if ('0' <= current && current <= '9') { + if (current >= '0' && current <= '9') { loM10 = (current - '0').toLong loDigits = 1 while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') @@ -422,7 +455,7 @@ object UnsafeNumbers { if (current == '.') { while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if (loM10 < 922337203685477580L) { loM10 = loM10 * 10 + (current - '0') @@ -436,49 +469,54 @@ object UnsafeNumbers { } } } - if ((current | 0x20) == 'e') { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - if (current < '0' || current > '9') throw UnsafeNumber - var exp = '0' - current - while ({ - current = in.read() - '0' <= current && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 + if ( + loDigits != 0 && ((current | 0x20) != 'e' || { + current = in.readChar().toInt + val negateExp = current == '-' + if (negateExp || current == '+') current = in.readChar().toInt + (current >= '0' && current <= '9') && { + var exp = '0' - current + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if ( + exp < -214748364 || { + exp = exp * 10 + ('0' - current) + exp > 0 + } + ) throw UnsafeNumber + } + negateExp && { + e10 += exp + e10 <= 0 + } || !negateExp && { + e10 -= exp + exp != -2147483648 } - ) throw UnsafeNumber - } - if (negateExp) { - e10 += exp - if (e10 > 0) throw UnsafeNumber - } else if (exp != -2147483648) e10 -= exp - else throw UnsafeNumber - } - if (consume && current != -1) throw UnsafeNumber - if (hiM10 eq null) { - if (loDigits == 0) throw UnsafeNumber - var x = - if (e10 == 0) loM10.toDouble - else { - if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { - val pow10 = pow10Doubles - if (e10 < 0) loM10 / pow10(-e10) - else if (e10 <= 22) loM10 * pow10(e10) - else { - val slop = 16 - loDigits - (loM10 * pow10(slop)) * pow10(e10 - slop) - } - } else toDouble(loM10, e10) } - if (negate) x = -x - return x + }) && (!consume || current == -1) + ) { + if (hiM10 eq null) { + var x = + if (e10 == 0) loM10.toDouble + else { + if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { + val pow10 = pow10Doubles + if (e10 < 0) loM10 / pow10(-e10) + else if (e10 <= 22) loM10 * pow10(e10) + else { + val slop = 16 - loDigits + (loM10 * pow10(slop)) * pow10(e10 - slop) + } + } else toDouble(loM10, e10) + } + if (negate) x = -x + return x + } + return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue } - toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue + throw UnsafeNumber } // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical From 323975d480e83ae8dc6ce47cda356b784da3f15e Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 12 Feb 2025 22:30:30 +0100 Subject: [PATCH 159/311] Fix encoding of `LocalDateTime` values to JSON AST (#1310) --- .../scala/zio/json/javatime/serializers.scala | 1 - .../test/scala/zio/json/JavaTimeSpec.scala | 152 +++++++++++++++--- 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala index 74ac25c51..3ac9b581b 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala @@ -138,7 +138,6 @@ private[json] object serializers { def toString(x: LocalDateTime): String = { val out = writes.get write(x, out) - write(x.toLocalDate, out) out.buffer.toString } diff --git a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala index 943a3d16f..6fb1ec273 100644 --- a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala @@ -1,5 +1,6 @@ package zio.json +import zio.json.ast._ import zio.test.Assertion._ import zio.test._ @@ -13,10 +14,12 @@ object JavaTimeSpec extends ZIOSpecDefault { private def equalToStringified(expected: String) = equalTo(s""""$expected"""") + private def equalToJsonStr(expected: String): Assertion[Either[String, Json]] = isRight(equalTo(Json.Str(expected))) + val spec: Spec[Environment, Any] = suite("java.time")( suite("Encoder")( - test("DayOfWeek") { + test("DayOfWeek toJson") { assert(DayOfWeek.MONDAY.toJson)(equalToStringified("MONDAY")) && assert(DayOfWeek.TUESDAY.toJson)(equalToStringified("TUESDAY")) && assert(DayOfWeek.WEDNESDAY.toJson)(equalToStringified("WEDNESDAY")) && @@ -25,41 +28,80 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(DayOfWeek.SATURDAY.toJson)(equalToStringified("SATURDAY")) && assert(DayOfWeek.SUNDAY.toJson)(equalToStringified("SUNDAY")) }, - test("Duration") { + test("DayOfWeek toJsonAST") { + assert(DayOfWeek.MONDAY.toJsonAST)(equalToJsonStr("MONDAY")) && + assert(DayOfWeek.TUESDAY.toJsonAST)(equalToJsonStr("TUESDAY")) && + assert(DayOfWeek.WEDNESDAY.toJsonAST)(equalToJsonStr("WEDNESDAY")) && + assert(DayOfWeek.THURSDAY.toJsonAST)(equalToJsonStr("THURSDAY")) && + assert(DayOfWeek.FRIDAY.toJsonAST)(equalToJsonStr("FRIDAY")) && + assert(DayOfWeek.SATURDAY.toJsonAST)(equalToJsonStr("SATURDAY")) && + assert(DayOfWeek.SUNDAY.toJsonAST)(equalToJsonStr("SUNDAY")) + }, + test("Duration toJson") { assert(Duration.ofDays(0).toJson)(equalToStringified("PT0S")) && assert(Duration.ofDays(1).toJson)(equalToStringified("PT24H")) && assert(Duration.ofHours(24).toJson)(equalToStringified("PT24H")) && assert(Duration.ofMinutes(1440).toJson)(equalToStringified("PT24H")) && assert(Duration.ofSeconds(Long.MaxValue, 999999999L).toJson)( equalToStringified("PT2562047788015215H30M7.999999999S") - ) && - assert(""""PT-0.5S"""".fromJson[Duration].map(_.toString))(isRight(equalTo("PT-0.5S"))) && - assert(""""-PT0.5S"""".fromJson[Duration].map(_.toString))(isRight(equalTo("PT-0.5S"))) + ) }, - test("Instant") { + test("Duration toJsonAST") { + assert(Duration.ofDays(0).toJsonAST)(equalToJsonStr("PT0S")) && + assert(Duration.ofDays(1).toJsonAST)(equalToJsonStr("PT24H")) && + assert(Duration.ofHours(24).toJsonAST)(equalToJsonStr("PT24H")) && + assert(Duration.ofMinutes(1440).toJsonAST)(equalToJsonStr("PT24H")) && + assert(Duration.ofSeconds(Long.MaxValue, 999999999L).toJsonAST)( + equalToJsonStr("PT2562047788015215H30M7.999999999S") + ) + }, + test("Instant toJson") { val n = Instant.now() assert(Instant.EPOCH.toJson)(equalToStringified("1970-01-01T00:00:00Z")) && assert(n.toJson)(equalToStringified(n.toString)) }, - test("LocalDate") { + test("Instant toJsonAST") { + val n = Instant.now() + assert(Instant.EPOCH.toJsonAST)(equalToJsonStr("1970-01-01T00:00:00Z")) && + assert(n.toJsonAST)(equalToJsonStr(n.toString)) + }, + test("LocalDate toJson") { val n = LocalDate.now() val p = LocalDate.of(2020, 1, 1) assert(n.toJson)(equalToStringified(n.format(DateTimeFormatter.ISO_LOCAL_DATE))) && assert(p.toJson)(equalToStringified("2020-01-01")) }, - test("LocalDateTime") { + test("LocalDate toJsonAST") { + val n = LocalDate.now() + val p = LocalDate.of(2020, 1, 1) + assert(n.toJsonAST)(equalToJsonStr(n.format(DateTimeFormatter.ISO_LOCAL_DATE))) && + assert(p.toJsonAST)(equalToJsonStr("2020-01-01")) + }, + test("LocalDateTime toJson") { val n = LocalDateTime.now() val p = LocalDateTime.of(2020, 1, 1, 12, 36, 0) assert(n.toJson)(equalToStringified(n.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))) && assert(p.toJson)(equalToStringified("2020-01-01T12:36:00")) }, - test("LocalTime") { + test("LocalDateTime toJsonAST") { + val n = LocalDateTime.now() + val p = LocalDateTime.of(2020, 1, 1, 12, 36, 0) + assert(n.toJsonAST)(equalToJsonStr(n.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))) && + assert(p.toJsonAST)(equalToJsonStr("2020-01-01T12:36:00")) + }, + test("LocalTime toJson") { val n = LocalTime.now() val p = LocalTime.of(12, 36, 0) assert(n.toJson)(equalToStringified(n.format(DateTimeFormatter.ISO_LOCAL_TIME))) && assert(p.toJson)(equalToStringified("12:36:00")) }, - test("Month") { + test("LocalTime toJsonAST") { + val n = LocalTime.now() + val p = LocalTime.of(12, 36, 0) + assert(n.toJsonAST)(equalToJsonStr(n.format(DateTimeFormatter.ISO_LOCAL_TIME))) && + assert(p.toJsonAST)(equalToJsonStr("12:36:00")) + }, + test("Month toJson") { assert(Month.JANUARY.toJson)(equalToStringified("JANUARY")) && assert(Month.FEBRUARY.toJson)(equalToStringified("FEBRUARY")) && assert(Month.MARCH.toJson)(equalToStringified("MARCH")) && @@ -73,44 +115,95 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(Month.NOVEMBER.toJson)(equalToStringified("NOVEMBER")) && assert(Month.DECEMBER.toJson)(equalToStringified("DECEMBER")) }, - test("MonthDay") { + test("Month toJsonAST") { + assert(Month.JANUARY.toJsonAST)(equalToJsonStr("JANUARY")) && + assert(Month.FEBRUARY.toJsonAST)(equalToJsonStr("FEBRUARY")) && + assert(Month.MARCH.toJsonAST)(equalToJsonStr("MARCH")) && + assert(Month.APRIL.toJsonAST)(equalToJsonStr("APRIL")) && + assert(Month.MAY.toJsonAST)(equalToJsonStr("MAY")) && + assert(Month.JUNE.toJsonAST)(equalToJsonStr("JUNE")) && + assert(Month.JULY.toJsonAST)(equalToJsonStr("JULY")) && + assert(Month.AUGUST.toJsonAST)(equalToJsonStr("AUGUST")) && + assert(Month.SEPTEMBER.toJsonAST)(equalToJsonStr("SEPTEMBER")) && + assert(Month.OCTOBER.toJsonAST)(equalToJsonStr("OCTOBER")) && + assert(Month.NOVEMBER.toJsonAST)(equalToJsonStr("NOVEMBER")) && + assert(Month.DECEMBER.toJsonAST)(equalToJsonStr("DECEMBER")) + }, + test("MonthDay toJson") { val n = MonthDay.now() val p = MonthDay.of(1, 1) assert(n.toJson)(equalToStringified(n.toString)) && assert(p.toJson)(equalToStringified("--01-01")) }, - test("OffsetDateTime") { + test("MonthDay toJsonAST") { + val n = MonthDay.now() + val p = MonthDay.of(1, 1) + assert(n.toJsonAST)(equalToJsonStr(n.toString)) && + assert(p.toJsonAST)(equalToJsonStr("--01-01")) + }, + test("OffsetDateTime toJson") { val n = OffsetDateTime.now() val p = OffsetDateTime.of(2020, 1, 1, 12, 36, 12, 0, ZoneOffset.UTC) assert(n.toJson)(equalToStringified(n.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))) && assert(p.toJson)(equalToStringified("2020-01-01T12:36:12Z")) }, - test("OffsetTime") { + test("OffsetDateTime toJsonAST") { + val n = OffsetDateTime.now() + val p = OffsetDateTime.of(2020, 1, 1, 12, 36, 12, 0, ZoneOffset.UTC) + assert(n.toJsonAST)(equalToJsonStr(n.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))) && + assert(p.toJsonAST)(equalToJsonStr("2020-01-01T12:36:12Z")) + }, + test("OffsetTime toJson") { val n = OffsetTime.now() val p = OffsetTime.of(12, 36, 12, 0, ZoneOffset.ofHours(-4)) assert(n.toJson)(equalToStringified(n.format(DateTimeFormatter.ISO_OFFSET_TIME))) && assert(p.toJson)(equalToStringified("12:36:12-04:00")) }, - test("Period") { + test("OffsetTime toJsonAST") { + val n = OffsetTime.now() + val p = OffsetTime.of(12, 36, 12, 0, ZoneOffset.ofHours(-4)) + assert(n.toJsonAST)(equalToJsonStr(n.format(DateTimeFormatter.ISO_OFFSET_TIME))) && + assert(p.toJsonAST)(equalToJsonStr("12:36:12-04:00")) + }, + test("Period toJson") { assert(Period.ZERO.toJson)(equalToStringified("P0D")) && assert(Period.ofDays(1).toJson)(equalToStringified("P1D")) && assert(Period.ofMonths(2).toJson)(equalToStringified("P2M")) && assert(Period.ofWeeks(52).toJson)(equalToStringified("P364D")) && assert(Period.ofYears(10).toJson)(equalToStringified("P10Y")) }, - test("Year") { + test("Period toJsonAST") { + assert(Period.ZERO.toJsonAST)(equalToJsonStr("P0D")) && + assert(Period.ofDays(1).toJsonAST)(equalToJsonStr("P1D")) && + assert(Period.ofMonths(2).toJsonAST)(equalToJsonStr("P2M")) && + assert(Period.ofWeeks(52).toJsonAST)(equalToJsonStr("P364D")) && + assert(Period.ofYears(10).toJsonAST)(equalToJsonStr("P10Y")) + }, + test("Year toJson") { val n = Year.now() assert(n.toJson)(equalToStringified(n.toString)) && assert(Year.of(1999).toJson)(equalToStringified("1999")) && assert(Year.of(10000).toJson)(equalToStringified("+10000")) }, - test("YearMonth") { + test("Year toJsonAST") { + val n = Year.now() + assert(n.toJsonAST)(equalToJsonStr(n.toString)) && + assert(Year.of(1999).toJsonAST)(equalToJsonStr("1999")) && + assert(Year.of(10000).toJsonAST)(equalToJsonStr("+10000")) + }, + test("YearMonth toJson") { val n = YearMonth.now() assert(n.toJson)(equalToStringified(n.toString)) && assert(YearMonth.of(1999, 12).toJson)(equalToStringified("1999-12")) && assert(YearMonth.of(1999, 1).toJson)(equalToStringified("1999-01")) }, - test("ZonedDateTime") { + test("YearMonth toJsonAST") { + val n = YearMonth.now() + assert(n.toJsonAST)(equalToJsonStr(n.toString)) && + assert(YearMonth.of(1999, 12).toJsonAST)(equalToJsonStr("1999-12")) && + assert(YearMonth.of(1999, 1).toJsonAST)(equalToJsonStr("1999-01")) + }, + test("ZonedDateTime toJson") { val n = ZonedDateTime.now() val ld = LocalDateTime.of(2020, 1, 1, 12, 36, 0) val est = ZonedDateTime.of(ld, ZoneId.of("America/New_York")) @@ -119,17 +212,38 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(est.toJson)(equalToStringified("2020-01-01T12:36:00-05:00[America/New_York]")) && assert(utc.toJson)(equalToStringified("2020-01-01T12:36:00Z[Etc/UTC]")) }, - test("ZoneId") { + test("ZonedDateTime toJsonAST") { + val n = ZonedDateTime.now() + val ld = LocalDateTime.of(2020, 1, 1, 12, 36, 0) + val est = ZonedDateTime.of(ld, ZoneId.of("America/New_York")) + val utc = ZonedDateTime.of(ld, ZoneId.of("Etc/UTC")) + assert(n.toJsonAST)(equalToJsonStr(n.format(DateTimeFormatter.ISO_ZONED_DATE_TIME))) && + assert(est.toJsonAST)(equalToJsonStr("2020-01-01T12:36:00-05:00[America/New_York]")) && + assert(utc.toJsonAST)(equalToJsonStr("2020-01-01T12:36:00Z[Etc/UTC]")) + }, + test("ZoneId toJson") { assert(ZoneId.of("America/New_York").toJson)(equalToStringified("America/New_York")) && assert(ZoneId.of("Etc/UTC").toJson)(equalToStringified("Etc/UTC")) && assert(ZoneId.of("Pacific/Auckland").toJson)(equalToStringified("Pacific/Auckland")) && assert(ZoneId.of("Asia/Shanghai").toJson)(equalToStringified("Asia/Shanghai")) && assert(ZoneId.of("Africa/Cairo").toJson)(equalToStringified("Africa/Cairo")) }, - test("ZoneOffset") { + test("ZoneId toJsonAST") { + assert(ZoneId.of("America/New_York").toJsonAST)(equalToJsonStr("America/New_York")) && + assert(ZoneId.of("Etc/UTC").toJsonAST)(equalToJsonStr("Etc/UTC")) && + assert(ZoneId.of("Pacific/Auckland").toJsonAST)(equalToJsonStr("Pacific/Auckland")) && + assert(ZoneId.of("Asia/Shanghai").toJsonAST)(equalToJsonStr("Asia/Shanghai")) && + assert(ZoneId.of("Africa/Cairo").toJsonAST)(equalToJsonStr("Africa/Cairo")) + }, + test("ZoneOffset toJson") { assert(ZoneOffset.UTC.toJson)(equalToStringified("Z")) && assert(ZoneOffset.ofHours(5).toJson)(equalToStringified("+05:00")) && assert(ZoneOffset.ofHours(-5).toJson)(equalToStringified("-05:00")) + }, + test("ZoneOffset toJsonAST") { + assert(ZoneOffset.UTC.toJsonAST)(equalToJsonStr("Z")) && + assert(ZoneOffset.ofHours(5).toJsonAST)(equalToJsonStr("+05:00")) && + assert(ZoneOffset.ofHours(-5).toJsonAST)(equalToJsonStr("-05:00")) } ), suite("Decoder")( From d027fad6a4994193b01f139c59fe6f983ed1361a Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 12 Feb 2025 23:49:23 +0100 Subject: [PATCH 160/311] Fix "diverging implicit expansion" error with Scala 2 (#1311) --- project/BuildHelper.scala | 8 +- .../scala-2.x/zio/json/JsonFieldDecoder.scala | 91 +++++++++++++++++++ .../zio/json/JsonFieldDecoder.scala | 0 .../src/test/scala/zio/json/DecoderSpec.scala | 12 +++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala rename zio-json/shared/src/main/{scala => scala-3}/zio/json/JsonFieldDecoder.scala (100%) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 5b3bd69ca..633c779ef 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -249,7 +249,13 @@ object BuildHelper { mimaBinaryIssueFilters ++= Seq( exclude[Problem]("zio.json.internal.*"), exclude[Problem]("zio.json.yaml.internal.*") - ), + ) ++ (CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, _)) => + Seq( + exclude[Problem]("zio.json.JsonFieldDecoder.stringLike") // FIXME: remove after v0.7.19 release + ) + case _ => Seq.empty + }), mimaFailOnProblem := true ) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala new file mode 100644 index 000000000..0dc35e599 --- /dev/null +++ b/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala @@ -0,0 +1,91 @@ +/* + * Copyright 2019-2022 John A. De Goes and the ZIO Contributors + * + * 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 zio.json + +import zio.json.internal.Lexer +import zio.json.uuid.UUIDParser + +/** When decoding a JSON Object, we only allow the keys that implement this interface. */ +trait JsonFieldDecoder[+A] { + self => + + final def map[B](f: A => B): JsonFieldDecoder[B] = + new JsonFieldDecoder[B] { + + def unsafeDecodeField(trace: List[JsonError], in: String): B = + f(self.unsafeDecodeField(trace, in)) + } + + final def mapOrFail[B](f: A => Either[String, B]): JsonFieldDecoder[B] = + new JsonFieldDecoder[B] { + + def unsafeDecodeField(trace: List[JsonError], in: String): B = + f(self.unsafeDecodeField(trace, in)) match { + case Left(err) => Lexer.error(err, trace) + case Right(b) => b + } + } + + def unsafeDecodeField(trace: List[JsonError], in: String): A +} + +object JsonFieldDecoder extends LowPriorityJsonFieldDecoder { + def apply[A](implicit a: JsonFieldDecoder[A]): JsonFieldDecoder[A] = a + + implicit val string: JsonFieldDecoder[String] = new JsonFieldDecoder[String] { + def unsafeDecodeField(trace: List[JsonError], in: String): String = in + } + + implicit val int: JsonFieldDecoder[Int] = new JsonFieldDecoder[Int] { + def unsafeDecodeField(trace: List[JsonError], in: String): Int = + try in.toInt + catch { + case _: NumberFormatException => Lexer.error(s"Invalid Int: ${strip(in)}", trace) + } + } + + implicit val long: JsonFieldDecoder[Long] = new JsonFieldDecoder[Long] { + def unsafeDecodeField(trace: List[JsonError], in: String): Long = + try in.toLong + catch { + case _: NumberFormatException => Lexer.error(s"Invalid Long: ${strip(in)}", trace) + } + } + + implicit val uuid: JsonFieldDecoder[java.util.UUID] = new JsonFieldDecoder[java.util.UUID] { + def unsafeDecodeField(trace: List[JsonError], in: String): java.util.UUID = + try UUIDParser.unsafeParse(in) + catch { + case _: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${strip(in)}", trace) + } + } + + // use this instead of `string.mapOrFail` in supertypes (to prevent class initialization error at runtime) + private[json] def mapStringOrFail[A](f: String => Either[String, A]): JsonFieldDecoder[A] = + new JsonFieldDecoder[A] { + def unsafeDecodeField(trace: List[JsonError], in: String): A = + f(string.unsafeDecodeField(trace, in)) match { + case Left(err) => Lexer.error(err, trace) + case Right(value) => value + } + } + + private[json] def strip(s: String, len: Int = 50): String = + if (s.length <= len) s + else s.substring(0, len) + "..." +} + +private[json] trait LowPriorityJsonFieldDecoder diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala similarity index 100% rename from zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala rename to zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 014568683..64d6548fc 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -930,4 +930,16 @@ object DecoderSpec extends ZIOSpecDefault { implicit val eventDecoder: JsonDecoder[Event] = DeriveJsonDecoder.gen[Event] implicit val eventEncoder: JsonEncoder[Event] = DeriveJsonEncoder.gen[Event] } + + object fieldDecoder { + case class PersonId(value: String) + + object PersonId { + implicit val jsonFieldEncoder: JsonFieldEncoder[PersonId] = JsonFieldEncoder.string.contramap(_.value) + implicit val jsonFieldDecoder: JsonFieldDecoder[PersonId] = JsonFieldDecoder.string.map(PersonId.apply) + } + + implicitly[JsonFieldEncoder[PersonId]] + implicitly[JsonFieldDecoder[PersonId]] + } } From 1a3738e0d334fbdd1b21f348a8ac021162477934 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 13 Feb 2025 14:15:36 +0100 Subject: [PATCH 161/311] More test coverage for numbers (#1313) --- .../zio/json/internal/UnsafeNumbers.scala | 2 +- .../zio/json/internal/UnsafeNumbers.scala | 2 +- .../json/EncoderPlatformSpecificSpec.scala | 12 +++++++++-- .../zio/json/internal/UnsafeNumbers.scala | 2 +- .../src/test/scala/zio/json/DecoderSpec.scala | 6 ++++-- .../zio/json/internal/SafeNumbersSpec.scala | 21 ++++++++++++++++++- 6 files changed, 37 insertions(+), 8 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 910b2c51e..1b290ef0f 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -94,7 +94,7 @@ object UnsafeNumbers { var accum = ('0' - current).toLong while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if ( accum < -922337203685477580L || { diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 218cc2357..7d0e29199 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -94,7 +94,7 @@ object UnsafeNumbers { var accum = ('0' - current).toLong while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if ( accum < -922337203685477580L || { diff --git a/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala b/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala index a731094df..02e7a2111 100644 --- a/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala +++ b/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala @@ -89,8 +89,16 @@ object EncoderPlatformSpecificSpec extends ZIOSpecDefault { test("writeJsonLines writes JSON lines") { val path = Files.createTempFile("log", "json") val events = Chunk( - Event(1603669876, "hello"), - Event(1603669875, "world") + Event(1, "hello", priority = 1111.1111111), + Event(12, "hello", priority = 11111111.111), + Event(123, "world", priority = 1.1111111111), + Event(1234, "world"), + Event(12345, "world"), + Event(123456, "world"), + Event(1234567, "world"), + Event(12345678, "world"), + Event(123456789, "world"), + Event(1234567890, "world", true) ) for { diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 218cc2357..7d0e29199 100644 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -94,7 +94,7 @@ object UnsafeNumbers { var accum = ('0' - current).toLong while ({ current = in.read() - '0' <= current && current <= '9' + current >= '0' && current <= '9' }) { if ( accum < -922337203685477580L || { diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 64d6548fc..a06ec3e3a 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -83,6 +83,7 @@ object DecoderSpec extends ZIOSpecDefault { assert("1.234567e9".fromJson[Float])(isRight(equalTo(1.234567e9f))) && assert("-1.234567e9".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && assert("\"-1.234567e9\"".fromJson[Float])(isRight(equalTo(-1.234567e9f))) && + assert("1.4e-45".fromJson[Float])(isRight(equalTo(1.4e-45f))) && assert("8.3e38".fromJson[Float])(isRight(equalTo(Float.PositiveInfinity))) && assert("-8.3e38".fromJson[Float])(isRight(equalTo(Float.NegativeInfinity))) && assert("1.23456789012345678901e-2147483648".fromJson[Float])(isLeft(equalTo("(expected a Float)"))) && @@ -106,6 +107,7 @@ object DecoderSpec extends ZIOSpecDefault { assert("1.23456789012345e9".fromJson[Double])(isRight(equalTo(1.23456789012345e9))) && assert("-1.23456789012345e9".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && assert("\"-1.23456789012345e9\"".fromJson[Double])(isRight(equalTo(-1.23456789012345e9))) && + assert("4.9e-324".fromJson[Double])(isRight(equalTo(4.9e-324))) && assert("1.8e308".fromJson[Double])(isRight(equalTo(Double.PositiveInfinity))) && assert("-1.8e308".fromJson[Double])(isRight(equalTo(Double.NegativeInfinity))) && assert("1.23456789012345678901e-2147483648".fromJson[Double])(isLeft(equalTo("(expected a Double)"))) && @@ -172,7 +174,7 @@ object DecoderSpec extends ZIOSpecDefault { }, test("BigInteger too large") { assert( - "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851316546851" .fromJson[java.math.BigInteger] )(isLeft(equalTo("(expected a 256-bit BigInteger)"))) && assert( @@ -925,7 +927,7 @@ object DecoderSpec extends ZIOSpecDefault { object logEvent { - case class Event(at: Long, message: String) + case class Event(at: Long, message: String, fatal: Boolean = false, priority: Double = 0.0) implicit val eventDecoder: JsonDecoder[Event] = DeriveJsonDecoder.gen[Event] implicit val eventEncoder: JsonEncoder[Event] = DeriveJsonEncoder.gen[Event] diff --git a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala index cf013d2f9..4a8de6fd3 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala @@ -1,5 +1,6 @@ package zio.json.internal +import zio.ZIO import zio.json.Gens._ import zio.test.Assertion._ import zio.test.TestAspect.jvmOnly @@ -100,6 +101,9 @@ object SafeNumbersSpec extends ZIOSpecDefault { }, test("invalid Byte (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.byte(s).isEmpty)(equalTo(true))) + }, + test("ByteNone") { + ZIO.attempt(ByteNone.value).flip.map(error => assertTrue(error.isInstanceOf[NoSuchElementException])) } ), suite("Double")( @@ -188,6 +192,9 @@ object SafeNumbersSpec extends ZIOSpecDefault { }, test("invalid doubles (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.double(s).isEmpty)(equalTo(true))) + }, + test("DoubleNone") { + ZIO.attempt(DoubleNone.value).flip.map(error => assertTrue(error.isInstanceOf[NoSuchElementException])) } ), suite("Float")( @@ -286,6 +293,9 @@ object SafeNumbersSpec extends ZIOSpecDefault { }, test("invalid float (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.float(s).isEmpty)(equalTo(true))) + }, + test("FloatNone") { + ZIO.attempt(FloatNone.value).flip.map(error => assertTrue(error.isInstanceOf[NoSuchElementException])) } ), suite("Int")( @@ -319,7 +329,10 @@ object SafeNumbersSpec extends ZIOSpecDefault { ) }, test("invalid (text)") { - check(genAlphaLowerString)(s => assert(SafeNumbers.int(s))(equalTo(IntNone))) + check(genAlphaLowerString)(s => assert(SafeNumbers.int(s).isEmpty)(equalTo(true))) + }, + test("IntNone") { + ZIO.attempt(IntNone.value).flip.map(error => assertTrue(error.isInstanceOf[NoSuchElementException])) } ), suite("Long")( @@ -354,6 +367,9 @@ object SafeNumbersSpec extends ZIOSpecDefault { }, test("invalid (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.long(s).isEmpty)(equalTo(true))) + }, + test("LongNone") { + ZIO.attempt(LongNone.value).flip.map(error => assertTrue(error.isInstanceOf[NoSuchElementException])) } ), suite("Short")( @@ -370,6 +386,9 @@ object SafeNumbersSpec extends ZIOSpecDefault { }, test("invalid (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.short(s).isEmpty)(equalTo(true))) + }, + test("ShortNone") { + ZIO.attempt(ShortNone.value).flip.map(error => assertTrue(error.isInstanceOf[NoSuchElementException])) } ) ) From f69c30e1b97713d2fb26fd30683a8955e2e76788 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 13 Feb 2025 16:16:53 +0100 Subject: [PATCH 162/311] Update magnolia to 1.3.13 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 43581927b..01e9d4423 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Seq( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.12" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.13" ) case _ => Seq( From b8f214d84c52b09dc587ee8a580acfcc61f7a1bb Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 13 Feb 2025 16:18:47 +0100 Subject: [PATCH 163/311] More efficient decoding of strings (#1314) --- .../shared/src/main/scala/zio/json/internal/lexer.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 5628ec091..518ec0745 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -191,7 +191,7 @@ object Lexer { def string(trace: List[JsonError], in: OneCharReader): CharSequence = { var c = in.nextNonWhitespace() if (c != '"') error("'\"'", c, trace) - var cs = new Array[Char](64) + var cs = charArrays.get var i = 0 while ({ c = in.readChar() @@ -206,6 +206,10 @@ object Lexer { new String(cs, 0, i) } + private[this] val charArrays = new ThreadLocal[Array[Char]] { + override def initialValue(): Array[Char] = new Array[Char](1024) + } + def char(trace: List[JsonError], in: OneCharReader): Char = { var c = in.nextNonWhitespace() if (c != '"') error("'\"'", c, trace) From 125b9206802e2c7fb82e6bcd27570614021fc37c Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 14 Feb 2025 09:14:51 +0100 Subject: [PATCH 164/311] Update zio-sbt-website to 0.4.0-alpha.31 (#1315) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 8e5bf891d..2b7c51655 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -10,6 +10,6 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") -addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.30") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.31") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.9" From 198a512668e246f8ea76450176cb4820bc66c56c Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 14 Feb 2025 09:44:40 +0100 Subject: [PATCH 165/311] Yet more efficient decoding of numbers (#1316) --- project/BuildHelper.scala | 8 +- .../scala/zio/json/internal/SafeNumbers.scala | 4 + .../zio/json/internal/UnsafeNumbers.scala | 88 +- .../zio/json/internal/FastStringWrite.scala | 0 .../scala/zio/json/internal/SafeNumbers.scala | 4 + .../zio/json/internal/UnsafeNumbers.scala | 88 +- .../resources/scala-native/multiply_high.c | 7 - .../zio/json/internal/FastStringWrite.scala | 171 ---- .../scala/zio/json/internal/NativeMath.scala | 13 - .../scala/zio/json/internal/SafeNumbers.scala | 773 ------------------ .../zio/json/internal/UnsafeNumbers.scala | 706 ---------------- .../src/main/scala/zio/json/JsonDecoder.scala | 251 +++++- .../main/scala/zio/json/internal/lexer.scala | 9 + .../src/test/scala/zio/json/DecoderSpec.scala | 30 +- .../zio/json/internal/SafeNumbersSpec.scala | 54 +- 15 files changed, 464 insertions(+), 1742 deletions(-) rename zio-json/{jvm => jvm-native}/src/main/scala/zio/json/internal/FastStringWrite.scala (100%) rename zio-json/{jvm => jvm-native}/src/main/scala/zio/json/internal/SafeNumbers.scala (99%) rename zio-json/{jvm => jvm-native}/src/main/scala/zio/json/internal/UnsafeNumbers.scala (92%) delete mode 100644 zio-json/native/src/main/resources/scala-native/multiply_high.c delete mode 100644 zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala delete mode 100644 zio-json/native/src/main/scala/zio/json/internal/NativeMath.scala delete mode 100644 zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala delete mode 100644 zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 633c779ef..5b3bd69ca 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -249,13 +249,7 @@ object BuildHelper { mimaBinaryIssueFilters ++= Seq( exclude[Problem]("zio.json.internal.*"), exclude[Problem]("zio.json.yaml.internal.*") - ) ++ (CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, _)) => - Seq( - exclude[Problem]("zio.json.JsonFieldDecoder.stringLike") // FIXME: remove after v0.7.19 release - ) - case _ => Seq.empty - }), + ), mimaFailOnProblem := true ) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 513f19795..60dd881aa 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -62,6 +62,10 @@ object SafeNumbers { try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def bigInt(num: String, max_bits: Int = 256): Option[BigInt] = + try Some(UnsafeNumbers.bigInt(num, max_bits)) + catch { case _: UnexpectedEnd | UnsafeNumber => None } + def float(num: String, max_bits: Int = 256): FloatOption = try FloatSome(UnsafeNumbers.float(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => FloatNone } diff --git a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 1b290ef0f..c903b73e2 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -37,18 +37,52 @@ object UnsafeNumbers { byte_(new FastStringReader(num), true) def byte_(in: OneCharReader, consume: Boolean): Byte = { - val n = int_(in, consume) - if (n < -128 || n > 127) throw UnsafeNumber - n.toByte + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current >= '0' && current <= '9') { + var accum = current - '0' + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + accum = accum * 10 + (current - '0') + if (accum > 128) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return (-accum).toByte + else if (accum < 128) return accum.toByte + } + } + throw UnsafeNumber } def short(num: String): Short = short_(new FastStringReader(num), true) def short_(in: OneCharReader, consume: Boolean): Short = { - val n = int_(in, consume) - if (n < -32768 || n > 32767) throw UnsafeNumber - n.toShort + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current >= '0' && current <= '9') { + var accum = current - '0' + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + accum = accum * 10 + (current - '0') + if (accum > 32768) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return (-accum).toShort + else if (accum < 32768) return accum.toShort + } + } + throw UnsafeNumber } def int(num: String): Int = @@ -153,6 +187,48 @@ object UnsafeNumbers { throw UnsafeNumber } + def bigInt(num: String, max_bits: Int): BigInt = + bigInt_(new FastStringReader(num), true, max_bits) + + def bigInt_(in: OneCharReader, consume: Boolean, max_bits: Int): BigInt = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current >= '0' && current <= '9') { + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if (loM10 < 922337203685477580L) { + loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0') + loDigits += 1 + } else { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = (current - '0').toLong + loDigits = 1 + } + } + if (!consume || current == -1) { + if (negate) loM10 = -loM10 + if (hiM10 eq null) return BigInt(loM10) + val bi = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)).unscaledValue + if (bi.bitLength < max_bits) return new BigInt(bi) + } + } + throw UnsafeNumber + } + def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = bigDecimal_(new FastStringReader(num), true, max_bits) diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/FastStringWrite.scala similarity index 100% rename from zio-json/jvm/src/main/scala/zio/json/internal/FastStringWrite.scala rename to zio-json/jvm-native/src/main/scala/zio/json/internal/FastStringWrite.scala diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala similarity index 99% rename from zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala rename to zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala index e6a72b054..f278fc1c6 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -62,6 +62,10 @@ object SafeNumbers { try Some(UnsafeNumbers.bigInteger(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def bigInt(num: String, max_bits: Int = 256): Option[BigInt] = + try Some(UnsafeNumbers.bigInt(num, max_bits)) + catch { case _: UnexpectedEnd | UnsafeNumber => None } + def float(num: String, max_bits: Int = 256): FloatOption = try FloatSome(UnsafeNumbers.float(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => FloatNone } diff --git a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/UnsafeNumbers.scala similarity index 92% rename from zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala rename to zio-json/jvm-native/src/main/scala/zio/json/internal/UnsafeNumbers.scala index 7d0e29199..c9d1a3164 100644 --- a/zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/UnsafeNumbers.scala @@ -37,18 +37,52 @@ object UnsafeNumbers { byte_(new FastStringReader(num), true) def byte_(in: OneCharReader, consume: Boolean): Byte = { - val n = int_(in, consume) - if (n < -128 || n > 127) throw UnsafeNumber - n.toByte + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current >= '0' && current <= '9') { + var accum = current - '0' + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + accum = accum * 10 + (current - '0') + if (accum > 128) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return (-accum).toByte + else if (accum < 128) return accum.toByte + } + } + throw UnsafeNumber } def short(num: String): Short = short_(new FastStringReader(num), true) def short_(in: OneCharReader, consume: Boolean): Short = { - val n = int_(in, consume) - if (n < -32768 || n > 32767) throw UnsafeNumber - n.toShort + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current >= '0' && current <= '9') { + var accum = current - '0' + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + accum = accum * 10 + (current - '0') + if (accum > 32768) throw UnsafeNumber + } + if (!consume || current == -1) { + if (negate) return (-accum).toShort + else if (accum < 32768) return accum.toShort + } + } + throw UnsafeNumber } def int(num: String): Int = @@ -153,6 +187,48 @@ object UnsafeNumbers { throw UnsafeNumber } + def bigInt(num: String, max_bits: Int): BigInt = + bigInt_(new FastStringReader(num), true, max_bits) + + def bigInt_(in: OneCharReader, consume: Boolean, max_bits: Int): BigInt = { + var current = + if (consume) in.readChar().toInt + else in.nextNonWhitespace().toInt + val negate = current == '-' + if (negate) current = in.readChar().toInt + if (current >= '0' && current <= '9') { + var loM10 = (current - '0').toLong + var loDigits = 1 + var hiM10: java.math.BigDecimal = null + while ({ + current = in.read() + current >= '0' && current <= '9' + }) { + if (loM10 < 922337203685477580L) { + loM10 = loM10 * 10 + (current - '0') + loDigits += 1 + } else { + if (negate) loM10 = -loM10 + val bd = java.math.BigDecimal.valueOf(loM10) + if (hiM10 eq null) hiM10 = bd + else { + hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) + if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber + } + loM10 = (current - '0').toLong + loDigits = 1 + } + } + if (!consume || current == -1) { + if (negate) loM10 = -loM10 + if (hiM10 eq null) return BigInt(loM10) + val bi = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)).unscaledValue + if (bi.bitLength < max_bits) return new BigInt(bi) + } + } + throw UnsafeNumber + } + def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = bigDecimal_(new FastStringReader(num), true, max_bits) diff --git a/zio-json/native/src/main/resources/scala-native/multiply_high.c b/zio-json/native/src/main/resources/scala-native/multiply_high.c deleted file mode 100644 index 6e2411c1c..000000000 --- a/zio-json/native/src/main/resources/scala-native/multiply_high.c +++ /dev/null @@ -1,7 +0,0 @@ -long zio_json_multiply_high(long x, long y) { - return x * (unsigned __int128) y >> 64; -} - -unsigned long zio_json_unsigned_multiply_high(unsigned long x, unsigned long y) { - return x * (unsigned __int128) y >> 64; -} diff --git a/zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala deleted file mode 100644 index 107d894d6..000000000 --- a/zio-json/native/src/main/scala/zio/json/internal/FastStringWrite.scala +++ /dev/null @@ -1,171 +0,0 @@ -package zio.json.internal - -import java.nio.CharBuffer -import java.util.Arrays - -final class FastStringWrite(initial: Int) extends Write { - require(initial >= 8) - private[this] var chars: Array[Char] = new Array[Char](initial) - private[this] var count: Int = 0 - - @inline def reset(): Unit = count = 0 - - @inline private[internal] def length: Int = count - - @inline private[internal] def getChars: Array[Char] = chars - - def write(s: String): Unit = { - val l = s.length - var cs = chars - val i = count - if (i + l >= cs.length) { - cs = Arrays.copyOf(cs, Math.max(cs.length << 1, i + l)) - chars = cs - } - s.getChars(0, l, cs, i) - count = i + l - } - - def write(c: Char): Unit = { - var cs = chars - val i = count - if (i + 1 >= cs.length) { - cs = Arrays.copyOf(cs, cs.length << 1) - chars = cs - } - cs(i) = c - count = i + 1 - } - - override def write(cs: Array[Char], from: Int, to: Int): Unit = { - var cs_ = chars - val from_ = count - val len = to - from - if (from_ + len >= cs_.length) { - cs_ = Arrays.copyOf(cs_, Math.max(cs_.length << 1, from_ + len)) - chars = cs_ - } - var i = 0 - while (i < len) { - cs_(from_ + i) = cs(from + i) - i += 1 - } - count = from_ + len - } - - override def write(c1: Char, c2: Char): Unit = { - var cs = chars - val i = count - if (i + 1 >= cs.length) { - cs = Arrays.copyOf(cs, cs.length << 1) - chars = cs - } - cs(i) = c1 - cs(i + 1) = c2 - count = i + 2 - } - - override def write(c1: Char, c2: Char, c3: Char): Unit = { - var cs = chars - val i = count - if (i + 2 >= cs.length) { - cs = Arrays.copyOf(cs, cs.length << 1) - chars = cs - } - cs(i) = c1 - cs(i + 1) = c2 - cs(i + 2) = c3 - count = i + 3 - } - - override def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = { - var cs = chars - val i = count - if (i + 3 >= cs.length) { - cs = Arrays.copyOf(cs, cs.length << 1) - chars = cs - } - cs(i) = c1 - cs(i + 1) = c2 - cs(i + 2) = c3 - cs(i + 3) = c4 - count = i + 4 - } - - override def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = { - var cs = chars - val i = count - if (i + 4 >= cs.length) { - cs = Arrays.copyOf(cs, cs.length << 1) - chars = cs - } - cs(i) = c1 - cs(i + 1) = c2 - cs(i + 2) = c3 - cs(i + 3) = c4 - cs(i + 4) = c5 - count = i + 5 - } - - override def write(s: Short): Unit = { - var cs = chars - val i = count - if (i + 1 >= cs.length) { - cs = Arrays.copyOf(cs, cs.length << 1) - chars = cs - } - cs(i) = (s & 0xff).toChar - cs(i + 1) = (s >> 8).toChar - count = i + 2 - } - - override def write(s1: Short, s2: Short): Unit = { - var cs = chars - val i = count - if (i + 3 >= cs.length) { - cs = Arrays.copyOf(cs, cs.length << 1) - chars = cs - } - cs(i) = (s1 & 0xff).toChar - cs(i + 1) = (s1 >> 8).toChar - cs(i + 2) = (s2 & 0xff).toChar - cs(i + 3) = (s2 >> 8).toChar - count = i + 4 - } - - override def write(s1: Short, s2: Short, s3: Short): Unit = { - var cs = chars - val i = count - if (i + 5 >= cs.length) { - cs = Arrays.copyOf(cs, cs.length << 1) - chars = cs - } - cs(i) = (s1 & 0xff).toChar - cs(i + 1) = (s1 >> 8).toChar - cs(i + 2) = (s2 & 0xff).toChar - cs(i + 3) = (s2 >> 8).toChar - cs(i + 4) = (s3 & 0xff).toChar - cs(i + 5) = (s3 >> 8).toChar - count = i + 6 - } - - override def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = { - var cs = chars - val i = count - if (i + 7 >= cs.length) { - cs = Arrays.copyOf(cs, cs.length << 1) - chars = cs - } - cs(i) = (s1 & 0xff).toChar - cs(i + 1) = (s1 >> 8).toChar - cs(i + 2) = (s2 & 0xff).toChar - cs(i + 3) = (s2 >> 8).toChar - cs(i + 4) = (s3 & 0xff).toChar - cs(i + 5) = (s3 >> 8).toChar - cs(i + 6) = (s4 & 0xff).toChar - cs(i + 7) = (s4 >> 8).toChar - count = i + 8 - } - - def buffer: CharSequence = CharBuffer.wrap(chars, 0, count) -} diff --git a/zio-json/native/src/main/scala/zio/json/internal/NativeMath.scala b/zio-json/native/src/main/scala/zio/json/internal/NativeMath.scala deleted file mode 100644 index d3302a880..000000000 --- a/zio-json/native/src/main/scala/zio/json/internal/NativeMath.scala +++ /dev/null @@ -1,13 +0,0 @@ -package zio.json.internal - -import scala.scalanative.unsafe._ - -// FIXME: Replace by an _efficient_ cross-platform version later, see: https://github.com/scala-native/scala-native/issues/2473 -@extern -private[internal] object NativeMath { - @name("zio_json_multiply_high") - def multiplyHigh(x: Long, y: Long): Long = extern - - @name("zio_json_unsigned_multiply_high") - def unsignedMultiplyHigh(x: Long, y: Long): Long = extern -} diff --git a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala deleted file mode 100644 index 547918b9e..000000000 --- a/zio-json/native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ /dev/null @@ -1,773 +0,0 @@ -/* - * Copyright 2019-2022 John A. De Goes and the ZIO Contributors - * - * 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 zio.json.internal - -import java.util.UUID - -/** - * Total, fast, number parsing. - * - * The Java and Scala standard libraries throw exceptions when we attempt to parse an invalid number. Unfortunately, - * exceptions are very expensive, and untrusted data can be maliciously constructed to DOS a server. - * - * This suite of functions mitigates against such attacks by building up the numbers one character at a time, which has - * been shown through extensive benchmarking to be orders of magnitude faster than exception-throwing stdlib parsers, - * for valid and invalid inputs. This approach, proposed by alexknvl, was also benchmarked against regexp-based - * pre-validation. - * - * Note that although the behaviour is identical to the Java stdlib when given the canonical form of a primitive (i.e. - * the .toString) of a number there may be differences in behaviour for non-canonical forms. e.g. the Java stdlib may - * reject "1.0" when parsed as an `BigInteger` but we may parse it as a `1`, although "1.1" would be rejected. Parsing - * of `BigDecimal` preserves the trailing zeros on the right but not on the left, e.g. "000.00001000" will be - * "1.000e-5", which is useful in cases where the trailing zeros denote measurement accuracy. - * - * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit limit on the size of the significand, to - * avoid OOM style attacks, which is 256 bits by default. - * - * Results are contained in a specialisation of Option that avoids boxing. - */ -object SafeNumbers { - import UnsafeNumbers.UnsafeNumber - - def byte(num: String): ByteOption = - try ByteSome(UnsafeNumbers.byte(num)) - catch { case _: UnexpectedEnd | UnsafeNumber => ByteNone } - - def short(num: String): ShortOption = - try ShortSome(UnsafeNumbers.short(num)) - catch { case _: UnexpectedEnd | UnsafeNumber => ShortNone } - - def int(num: String): IntOption = - try IntSome(UnsafeNumbers.int(num)) - catch { case _: UnexpectedEnd | UnsafeNumber => IntNone } - - def long(num: String): LongOption = - try LongSome(UnsafeNumbers.long(num)) - catch { case _: UnexpectedEnd | UnsafeNumber => LongNone } - - def bigInteger(num: String, max_bits: Int = 256): Option[java.math.BigInteger] = - try Some(UnsafeNumbers.bigInteger(num, max_bits)) - catch { case _: UnexpectedEnd | UnsafeNumber => None } - - def float(num: String, max_bits: Int = 256): FloatOption = - try FloatSome(UnsafeNumbers.float(num, max_bits)) - catch { case _: UnexpectedEnd | UnsafeNumber => FloatNone } - - def double(num: String, max_bits: Int = 256): DoubleOption = - try DoubleSome(UnsafeNumbers.double(num, max_bits)) - catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone } - - def bigDecimal(num: String, max_bits: Int = 256): Option[java.math.BigDecimal] = - try Some(UnsafeNumbers.bigDecimal(num, max_bits)) - catch { case _: UnexpectedEnd | UnsafeNumber => None } - - def toString(x: Double): String = { - val out = new FastStringWrite(24) - write(x, out) - out.buffer.toString - } - - def toString(x: Float): String = { - val out = new FastStringWrite(16) - write(x, out) - out.buffer.toString - } - - def toString(x: UUID): String = { - val out = writes.get - write(x, out) - out.buffer.toString - } - - // Based on the amazing work of Raffaello Giulietti - // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view - // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java - def write(x: Double, out: Write): Unit = { - val bits = java.lang.Double.doubleToLongBits(x) - val ieeeExponent = (bits >> 52).toInt & 0x7ff - val ieeeMantissa = bits & 0xfffffffffffffL - if (ieeeExponent == 2047) { - out.write( - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" - ) - } else { - if (bits < 0) out.write('-') - if (x == 0.0f) out.write('0', '.', '0') - else { - var e = ieeeExponent - 1075 - var m = ieeeMantissa | 0x10000000000000L - var dv = 0L - var exp = 0 - if (e == 0) dv = m - else if (e >= -52 && e < 0 && m << e == 0) dv = m >> -e - else { - var expShift, expCorr = 0 - var cblShift = 2 - if (ieeeExponent == 0) { - e = -1074 - m = ieeeMantissa - if (ieeeMantissa < 3) { - m *= 10 - expShift = 1 - } - } else if (ieeeMantissa == 0 && ieeeExponent > 1) { - expCorr = 131007 - cblShift = 1 - } - exp = e * 315653 - expCorr >> 20 - val i = exp + 324 << 1 - val g1 = gs(i) - val g0 = gs(i + 1) - val h = (-exp * 108853 >> 15) + e + 2 - val cb = m << 2 - val outm1 = (m.toInt & 0x1) - 1 - val vb = rop(g1, g0, cb << h) - val vbls = rop(g1, g0, cb - cblShift << h) + outm1 - val vbrd = outm1 - rop(g1, g0, cb + 2 << h) - val s = vb >> 2 - if ( - s < 100 || { - dv = Math.multiplyHigh(s, 1844674407370955168L) // divide a positive long by 10 - val sp40 = dv * 40 - val upin = (vbls - sp40).toInt - (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { - dv += ~upin >>> 31 - exp += 1 - false - } - } - ) { - val s4 = s << 2 - val uin = (vbls - s4).toInt - dv = (~ { - if ((((s4 + vbrd).toInt + 4) ^ uin) < 0) uin - else (vb.toInt & 0x3) + (s.toInt & 0x1) - 3 - } >>> 31) + s - exp -= expShift - } - } - val len = digitCount(dv) - exp += len - 1 - if (exp < -3 || exp >= 7) { - val sdv = stripTrailingZeros(dv) - if (sdv < 10) out.write((sdv.toInt | '0').toChar, '.', '0', 'E') - else { - val w = writes.get - write(sdv, w) - val cs = w.getChars - out.write(cs(0), '.') - out.write(cs, 1, w.length) - out.write('E') - } - write(exp, out) - } else if (exp < 0) { - out.write('0', '.') - while ({ - exp += 1 - exp != 0 - }) out.write('0') - write(stripTrailingZeros(dv), out) - } else { - exp += 1 - if (exp < len) { - val w = writes.get - write(stripTrailingZeros(dv), w) - val cs = w.getChars - out.write(cs, 0, exp) - out.write('.') - out.write(cs, exp, w.length) - } else { - write(dv.toInt, out) - out.write('.', '0') - } - } - } - } - } - - def write(x: Float, out: Write): Unit = { - val bits = java.lang.Float.floatToIntBits(x) - val ieeeExponent = (bits >> 23) & 0xff - val ieeeMantissa = bits & 0x7fffff - if (ieeeExponent == 255) { - out.write( - if (x != x) """"NaN"""" - else if (bits < 0) """"-Infinity"""" - else """"Infinity"""" - ) - } else { - if (bits < 0) out.write('-') - if (x == 0.0f) out.write('0', '.', '0') - else { - var e = ieeeExponent - 150 - var m = ieeeMantissa | 0x800000 - var dv, exp = 0 - if (e == 0) dv = m - else if (e >= -23 && e < 0 && m << e == 0) dv = m >> -e - else { - var expShift, expCorr = 0 - var cblShift = 2 - if (ieeeExponent == 0) { - e = -149 - m = ieeeMantissa - if (ieeeMantissa < 8) { - m *= 10 - expShift = 1 - } - } else if (ieeeMantissa == 0 && ieeeExponent > 1) { - expCorr = 131007 - cblShift = 1 - } - exp = e * 315653 - expCorr >> 20 - val g1 = gs(exp + 324 << 1) + 1 - val h = (-exp * 108853 >> 15) + e + 1 - val cb = m << 2 - val outm1 = (m & 0x1) - 1 - val vb = rop(g1, cb << h) - val vbls = rop(g1, cb - cblShift << h) + outm1 - val vbrd = outm1 - rop(g1, cb + 2 << h) - val s = vb >> 2 - if ( - s < 100 || { - dv = (s * 3435973837L >>> 35).toInt // divide a positive int by 10 - val sp40 = dv * 40 - val upin = vbls - sp40 - ((sp40 + vbrd + 40) ^ upin) >= 0 || { - dv += ~upin >>> 31 - exp += 1 - false - } - } - ) { - val s4 = s << 2 - val uin = vbls - s4 - dv = (~ { - if (((s4 + vbrd + 4) ^ uin) < 0) uin - else (vb & 0x3) + (s & 0x1) - 3 - } >>> 31) + s - exp -= expShift - } - } - val len = digitCount(dv.toLong) - exp += len - 1 - if (exp < -3 || exp >= 7) { - val sdv = stripTrailingZeros(dv) - if (sdv < 10) out.write((sdv | '0').toChar, '.', '0', 'E') - else { - val w = writes.get - write(sdv, w) - val cs = w.getChars - out.write(cs(0), '.') - out.write(cs, 1, w.length) - out.write('E') - } - write(exp, out) - } else if (exp < 0) { - out.write('0', '.') - while ({ - exp += 1 - exp != 0 - }) out.write('0') - write(stripTrailingZeros(dv), out) - } else { - exp += 1 - if (exp < len) { - val w = writes.get - write(stripTrailingZeros(dv), w) - val cs = w.getChars - out.write(cs, 0, exp) - out.write('.') - out.write(cs, exp, w.length) - } else { - write(dv, out) - out.write('.', '0') - } - } - } - } - } - - def write(x: UUID, out: Write): Unit = { - val ds = lowerCaseHexDigits - val msb = x.getMostSignificantBits - val lsb = x.getLeastSignificantBits - val msb1 = (msb >> 32).toInt - out.write(ds(msb1 >>> 24), ds(msb1 >> 16 & 0xff), ds(msb1 >> 8 & 0xff), ds(msb1 & 0xff)) - out.write('-') - val msb2 = msb.toInt - out.write(ds(msb2 >>> 24), ds(msb2 >> 16 & 0xff)) - out.write('-') - out.write(ds(msb2 >> 8 & 0xff), ds(msb2 & 0xff)) - out.write('-') - val lsb1 = (lsb >>> 32).toInt - out.write(ds(lsb1 >>> 24), ds(lsb1 >> 16 & 0xff)) - out.write('-') - out.write(ds(lsb1 >> 8 & 0xff), ds(lsb1 & 0xff)) - val lsb2 = lsb.toInt - out.write(ds(lsb2 >>> 24), ds(lsb2 >> 16 & 0xff), ds(lsb2 >> 8 & 0xff), ds(lsb2 & 0xff)) - } - - private[json] def writeNano(x: Int, out: Write): Unit = { - out.write('.') - var coeff = 100000000 - while (coeff > x) { - out.write('0') - coeff = (coeff * 3435973837L >> 35).toInt // divide a positive int by 10 - } - write(stripTrailingZeros(x), out) - } - - private[this] val writes = new ThreadLocal[FastStringWrite] { - override def initialValue(): FastStringWrite = new FastStringWrite(24) - - override def get: FastStringWrite = { - val w = super.get - w.reset() - w - } - } - - private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { - val x = Math.multiplyHigh(g0, cp) + (g1 * cp >>> 1) - Math.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63 - } - - private[this] def rop(g: Long, cp: Int): Int = { - val x = Math.multiplyHigh(g, cp.toLong << 32) - (x >>> 31).toInt | -x.toInt >>> 31 - } - - private[this] def stripTrailingZeros(x: Long): Long = { - var q0 = x.toInt - if ( - q0 == x || { - q0 = (Math.multiplyHigh(x, 6189700196426901375L) >>> 25).toInt // divide a positive long by 100000000 - (x - q0 * 100000000L).toInt == 0 - } - ) return stripTrailingZeros(q0).toLong - var y, q1 = x - while ({ - q1 = Math.multiplyHigh(q1, 1844674407370955168L) // divide a positive long by 10 - q1 * 10 == y - }) y = q1 - y - } - - private[this] def stripTrailingZeros(x: Int): Int = { - var q0, q1 = x - while ({ - val qp = q1 * 3435973837L - q1 = (qp >> 35).toInt // divide a positive int by 10 - (qp & 0x7e0000000L) == 0 // check if q is divisible by 10 - }) q0 = q1 - q0 - } - - def write(a: Long, out: Write): Unit = { - var q0 = a - if (q0 < 0) { - q0 = -q0 - out.write('-') - if (q0 == a) { - out.write('9', '2', '2') - q0 = 3372036854775808L - } - } - val m1 = 100000000L - if (q0 < m1) write(q0.toInt, out) - else { - val m2 = 6189700196426901375L - val q1 = Math.multiplyHigh(q0, m2) >>> 25 // divide a positive long by 100000000 - if (q1 < m1) write(q1.toInt, out) - else { - val q2 = Math.multiplyHigh(q1, m2) >>> 25 // divide a small positive long by 100000000 - write(q2.toInt, out) - write8Digits((q1 - q2 * m1).toInt, out) - } - write8Digits((q0 - q1 * m1).toInt, out) - } - } - - def write(a: Int, out: Write): Unit = { - val ds = digits - var q0 = a - if (q0 < 0) { - q0 = -q0 - out.write('-') - if (q0 == a) { - out.write('2') - q0 = 147483648 - } - } - if (q0 < 100) { // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ - if (q0 < 10) out.write((q0 | '0').toChar) - else out.write(ds(q0)) - } else if (q0 < 10000) { - val q1 = q0 * 5243 >> 19 // divide a small positive int by 100 - val d2 = ds(q0 - q1 * 100) - if (q0 < 1000) out.write((q1 | '0').toChar) - else out.write(ds(q1)) - out.write(d2) - } else if (q0 < 1000000) { - val y1 = q0 * 429497L - val y2 = (y1 & 0xffffffffL) * 100 - val y3 = (y2 & 0xffffffffL) * 100 - if (q0 < 100000) out.write(((y1 >> 32).toInt | '0').toChar) - else out.write(ds((y1 >> 32).toInt)) - out.write(ds((y2 >> 32).toInt), ds((y3 >> 32).toInt)) - } else if (q0 < 100000000) { - val y1 = q0 * 140737489L - val y2 = (y1 & 0x7fffffffffffL) * 100 - val y3 = (y2 & 0x7fffffffffffL) * 100 - val y4 = (y3 & 0x7fffffffffffL) * 100 - if (q0 < 10000000) out.write(((y1 >> 47).toInt | '0').toChar) - else out.write(ds((y1 >> 47).toInt)) - out.write(ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) - } else { - val y1 = q0 * 1441151881L - val y2 = (y1 & 0x1ffffffffffffffL) * 100 - val y3 = (y2 & 0x1ffffffffffffffL) * 100 - val y4 = (y3 & 0x1ffffffffffffffL) * 100 - val y5 = (y4 & 0x1ffffffffffffffL) * 100 - if (q0 < 1000000000) out.write(((y1 >>> 57).toInt | '0').toChar) - else out.write(ds((y1 >>> 57).toInt)) - out.write(ds((y2 >>> 57).toInt), ds((y3 >>> 57).toInt), ds((y4 >>> 57).toInt), ds((y5 >>> 57).toInt)) - } - } - - private[this] def write8Digits(x: Int, out: Write): Unit = { - val ds = digits // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ - val y1 = x * 140737489L - val m1 = 0x7fffffffffffL - val m2 = 100L - val y2 = (y1 & m1) * m2 - val y3 = (y2 & m1) * m2 - val y4 = (y3 & m1) * m2 - out.write(ds((y1 >> 47).toInt), ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) - } - - @inline private[json] def write4Digits(x: Int, out: Write): Unit = { - val ds = digits - val q = x * 5243 >> 19 // divide a 4-digit positive int by 100 - out.write(ds(q), ds(x - q * 100)) - } - - @inline private[json] def write3Digits(x: Int, out: Write): Unit = { - val q = x * 1311 >> 17 // divide a 3-digit positive int by 100 - out.write((q + '0').toChar) - out.write(digits(x - q * 100)) - } - - @inline private[json] def write2Digits(x: Int, out: Write): Unit = - out.write(digits(x)) - - private[this] final val digits: Array[Short] = Array( - 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, - 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, - 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, - 14388, 14644, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 12342, 12598, 12854, 13110, - 13366, 13622, 13878, 14134, 14390, 14646, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, - 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 12345, 12601, 12857, 13113, 13369, 13625, - 13881, 14137, 14393, 14649 - ) - - // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: - // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ - private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt - - private[this] val offsets = Array( - 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, - 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 4889916394579099648L, 4889916394579099648L, - 4889916394579099648L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, - 4323355642275676160L, 4323355642275676160L, 4323355642275676160L, 4035215266123964416L, 4035215266123964416L, - 4035215266123964416L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, - 3458764413820540928L, 3458764413820540928L, 3458764413820540928L, 3170534127668829184L, 3170534127668829184L, - 3170534127668829184L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, - 2594073385265405696L, 2594073385265405696L, 2594073385265405696L, 2305843009203693952L, 2305843009203693952L, - 2305843009203693952L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, - 1729382256910170464L, 1729382256910170464L, 1729382256910170464L, 1441151880758548720L, 1441151880758548720L, - 1441151880758548720L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, - 864691128455135132L, 864691128455135132L, 864691128455135132L, 576460752303423478L, 576460752303423478L, - 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L - ) - - private[this] final val lowerCaseHexDigits: Array[Short] = Array( - 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 24880, 25136, 25392, 25648, 25904, 26160, - 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 24881, 25137, 25393, 25649, 25905, 26161, - 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 24882, 25138, 25394, 25650, 25906, 26162, - 12339, 12595, 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 24883, 25139, 25395, 25651, 25907, 26163, - 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, 14388, 14644, 24884, 25140, 25396, 25652, 25908, 26164, - 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 24885, 25141, 25397, 25653, 25909, 26165, - 12342, 12598, 12854, 13110, 13366, 13622, 13878, 14134, 14390, 14646, 24886, 25142, 25398, 25654, 25910, 26166, - 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, 24887, 25143, 25399, 25655, 25911, 26167, - 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 24888, 25144, 25400, 25656, 25912, 26168, - 12345, 12601, 12857, 13113, 13369, 13625, 13881, 14137, 14393, 14649, 24889, 25145, 25401, 25657, 25913, 26169, - 12385, 12641, 12897, 13153, 13409, 13665, 13921, 14177, 14433, 14689, 24929, 25185, 25441, 25697, 25953, 26209, - 12386, 12642, 12898, 13154, 13410, 13666, 13922, 14178, 14434, 14690, 24930, 25186, 25442, 25698, 25954, 26210, - 12387, 12643, 12899, 13155, 13411, 13667, 13923, 14179, 14435, 14691, 24931, 25187, 25443, 25699, 25955, 26211, - 12388, 12644, 12900, 13156, 13412, 13668, 13924, 14180, 14436, 14692, 24932, 25188, 25444, 25700, 25956, 26212, - 12389, 12645, 12901, 13157, 13413, 13669, 13925, 14181, 14437, 14693, 24933, 25189, 25445, 25701, 25957, 26213, - 12390, 12646, 12902, 13158, 13414, 13670, 13926, 14182, 14438, 14694, 24934, 25190, 25446, 25702, 25958, 26214 - ) - - private[this] val gs: Array[Long] = Array( - 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, - 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, - 7466108948025751031L, 8492109508320019073L, 5972887158420600825L, 4949013199285060097L, 4778309726736480660L, - 3959210559428048077L, 7645295562778369056L, 6334736895084876923L, 6116236450222695245L, 3223115108696946377L, - 4892989160178156196L, 2578492086957557102L, 7828782656285049914L, 436238524390181040L, 6263026125028039931L, - 2193665226883099993L, 5010420900022431944L, 9133629810990300641L, 8016673440035891111L, 9079784475471615541L, - 6413338752028712889L, 5419153173006337271L, 5130671001622970311L, 6179996945776024979L, 8209073602596752498L, - 6198646298499729642L, 6567258882077401998L, 8648265853541694037L, 5253807105661921599L, 1384589460720489745L, - 8406091369059074558L, 5904691951894693915L, 6724873095247259646L, 8413102376257665455L, 5379898476197807717L, - 4885807493635177203L, 8607837561916492348L, 438594360332462878L, 6886270049533193878L, 4040224303007880625L, - 5509016039626555102L, 6921528257148214824L, 8814425663402488164L, 3695747581953323071L, 7051540530721990531L, - 4801272472933613619L, 5641232424577592425L, 1996343570975935733L, 9025971879324147880L, 3194149713561497173L, - 7220777503459318304L, 2555319770849197738L, 5776622002767454643L, 3888930224050313352L, 4621297602213963714L, - 6800492993982161005L, 7394076163542341943L, 5346765568258592123L, 5915260930833873554L, 7966761269348784022L, - 4732208744667098843L, 8218083422849982379L, 7571533991467358150L, 2080887032334240837L, 6057227193173886520L, - 1664709625867392670L, 4845781754539109216L, 1331767700693914136L, 7753250807262574745L, 7664851543223128102L, - 6202600645810059796L, 6131881234578502482L, 4962080516648047837L, 3060830580291846824L, 7939328826636876539L, - 6742003335837910079L, 6351463061309501231L, 7238277076041283225L, 5081170449047600985L, 3945947253462071419L, - 8129872718476161576L, 6313515605539314269L, 6503898174780929261L, 3206138077060496254L, 5203118539824743409L, - 720236054277441842L, 8324989663719589454L, 4841726501585817270L, 6659991730975671563L, 5718055608639608977L, - 5327993384780537250L, 8263793301653597505L, 8524789415648859601L, 3998697245790980200L, 6819831532519087681L, - 1354283389261828999L, 5455865226015270144L, 8462124340893283845L, 8729384361624432231L, 8005375723316388668L, - 6983507489299545785L, 4559626171282155773L, 5586805991439636628L, 3647700937025724618L, 8938889586303418605L, - 3991647091870204227L, 7151111669042734884L, 3193317673496163382L, 5720889335234187907L, 4399328546167885867L, - 9153422936374700651L, 8883600081239572549L, 7322738349099760521L, 5262205657620702877L, 5858190679279808417L, - 2365090118725607140L, 4686552543423846733L, 7426095317093351197L, 7498484069478154774L, 813706063123630946L, - 5998787255582523819L, 2495639257869859918L, 4799029804466019055L, 3841185813666843096L, 7678447687145630488L, - 6145897301866948954L, 6142758149716504390L, 8606066656235469486L, 4914206519773203512L, 6884853324988375589L, - 7862730431637125620L, 3637067690497580296L, 6290184345309700496L, 2909654152398064237L, 5032147476247760397L, - 483048914547496228L, 8051435961996416635L, 2617552670646949126L, 6441148769597133308L, 2094042136517559301L, - 5152919015677706646L, 5364582523955957764L, 8244670425084330634L, 4893983223587622099L, 6595736340067464507L, - 5759860986241052841L, 5276589072053971606L, 918539974250931950L, 8442542515286354569L, 7003687180914356604L, - 6754034012229083655L, 7447624152102440445L, 5403227209783266924L, 5958099321681952356L, 8645163535653227079L, - 3998935692578258285L, 6916130828522581663L, 5043822961433561789L, 5532904662818065330L, 7724407183888759755L, - 8852647460508904529L, 3135679457367239799L, 7082117968407123623L, 4353217973264747001L, 5665694374725698898L, - 7171923193353707924L, 9065110999561118238L, 407030665140201709L, 7252088799648894590L, 4014973346854071690L, - 5801671039719115672L, 3211978677483257352L, 4641336831775292537L, 8103606164099471367L, 7426138930840468060L, - 5587072233075333540L, 5940911144672374448L, 4469657786460266832L, 4752728915737899558L, 7265075043910123789L, - 7604366265180639294L, 556073626030467093L, 6083493012144511435L, 2289533308195328836L, 4866794409715609148L, - 1831626646556263069L, 7786871055544974637L, 1085928227119065748L, 6229496844435979709L, 6402765803808118083L, - 4983597475548783767L, 6966887050417449628L, 7973755960878054028L, 3768321651184098759L, 6379004768702443222L, - 6704006135689189330L, 5103203814961954578L, 1673856093809441141L, 8165126103939127325L, 833495342724150664L, - 6532100883151301860L, 666796274179320531L, 5225680706521041488L, 533437019343456425L, 8361089130433666380L, - 8232196860433350926L, 6688871304346933104L, 6585757488346680741L, 5351097043477546483L, 7113280398048299755L, - 8561755269564074374L, 313202192651548637L, 6849404215651259499L, 2095236161492194072L, 5479523372521007599L, - 3520863336564710419L, 8767237396033612159L, 99358116390671185L, 7013789916826889727L, 1924160900483492110L, - 5611031933461511781L, 7073351942499659173L, 8977651093538418850L, 7628014293257544353L, 7182120874830735080L, - 6102411434606035483L, 5745696699864588064L, 4881929147684828386L, 9193114719783340903L, 2277063414182859933L, - 7354491775826672722L, 5510999546088198270L, 5883593420661338178L, 719450822128648293L, 4706874736529070542L, - 4264909472444828957L, 7530999578446512867L, 8668529563282681493L, 6024799662757210294L, 3245474835884234871L, - 4819839730205768235L, 4441054276078343059L, 7711743568329229176L, 7105686841725348894L, 6169394854663383341L, - 3839875066009323953L, 4935515883730706673L, 1227225645436504001L, 7896825413969130677L, 118886625327451240L, - 6317460331175304541L, 5629132522374826477L, 5053968264940243633L, 2658631610528906020L, 8086349223904389813L, - 2409136169475294470L, 6469079379123511850L, 5616657750322145900L, 5175263503298809480L, 4493326200257716720L, - 8280421605278095168L, 7189321920412346751L, 6624337284222476135L, 217434314217011916L, 5299469827377980908L, - 173947451373609533L, 8479151723804769452L, 7657013551681595899L, 6783321379043815562L, 2436262026603366396L, - 5426657103235052449L, 7483032843395558602L, 8682651365176083919L, 6438829327320028278L, 6946121092140867135L, - 6995737869226977784L, 5556896873712693708L, 5596590295381582227L, 8891034997940309933L, 7109870065239576402L, - 7112827998352247947L, 153872830078795637L, 5690262398681798357L, 5657121486175901994L, 9104419837890877372L, - 1672696748397622544L, 7283535870312701897L, 6872180620830963520L, 5826828696250161518L, 1808395681922860493L, - 4661462957000129214L, 5136065360280198718L, 7458340731200206743L, 2683681354335452463L, 5966672584960165394L, - 5836293898210272294L, 4773338067968132315L, 6513709525939172997L, 7637340908749011705L, 1198563204647900987L, - 6109872726999209364L, 958850563718320789L, 4887898181599367491L, 2611754858345611793L, 7820637090558987986L, - 489458958611068546L, 6256509672447190388L, 7770264796372675483L, 5005207737957752311L, 682188614985274902L, - 8008332380732403697L, 6625525006089305327L, 6406665904585922958L, 1611071190129533939L, 5125332723668738366L, - 4978205766845537474L, 8200532357869981386L, 4275780412210949635L, 6560425886295985109L, 1575949922397804547L, - 5248340709036788087L, 3105434345289198799L, 8397345134458860939L, 6813369359833673240L, 6717876107567088751L, - 7295369895237893754L, 5374300886053671001L, 3991621508819359841L, 8598881417685873602L, 2697245599369065423L, - 6879105134148698881L, 7691819701608117823L, 5503284107318959105L, 4308781353915539097L, 8805254571710334568L, - 6894050166264862555L, 7044203657368267654L, 9204588947753800367L, 5635362925894614123L, 9208345565573995455L, - 9016580681431382598L, 3665306460692661759L, 7213264545145106078L, 6621593983296039730L, 5770611636116084862L, - 8986624001378742108L, 4616489308892867890L, 3499950386361083363L, 7386382894228588624L, 5599920618177733380L, - 5909106315382870899L, 6324610901913141866L, 4727285052306296719L, 6904363128901468655L, 7563656083690074751L, - 5512957784129484362L, 6050924866952059801L, 2565691819932632328L, 4840739893561647841L, 207879048575150701L, - 7745183829698636545L, 5866629699833106606L, 6196147063758909236L, 4693303759866485285L, 4956917651007127389L, - 1909968600522233067L, 7931068241611403822L, 6745298575577483229L, 6344854593289123058L, 1706890045720076260L, - 5075883674631298446L, 5054860851317971332L, 8121413879410077514L, 4398428547366843807L, 6497131103528062011L, - 5363417245264430207L, 5197704882822449609L, 2446059388840589004L, 8316327812515919374L, 7603043836886852730L, - 6653062250012735499L, 7927109476880437346L, 5322449800010188399L, 8186361988875305038L, 8515919680016301439L, - 7564155960087622576L, 6812735744013041151L, 7895999175441053223L, 5450188595210432921L, 4472124932981887417L, - 8720301752336692674L, 3466051078029109543L, 6976241401869354139L, 4617515269794242796L, 5580993121495483311L, - 5538686623206349399L, 8929588994392773298L, 5172549782388248714L, 7143671195514218638L, 7827388640652509295L, - 5714936956411374911L, 727887690409141951L, 9143899130258199857L, 6698643526767492606L, 7315119304206559886L, - 1669566006672083762L, 5852095443365247908L, 8714350434821487656L, 4681676354692198327L, 1437457125744324640L, - 7490682167507517323L, 4144605808561874585L, 5992545734006013858L, 7005033461591409992L, 4794036587204811087L, - 70003547160262509L, 7670458539527697739L, 1956680082827375175L, 6136366831622158191L, 3410018473632855302L, - 4909093465297726553L, 883340371535329080L, 7854549544476362484L, 8792042223940347174L, 6283639635581089987L, - 8878308186523232901L, 5026911708464871990L, 3413297734476675998L, 8043058733543795184L, 5461276375162681596L, - 6434446986835036147L, 6213695507501100438L, 5147557589468028918L, 1281607591258970028L, 8236092143148846269L, - 205897738643396882L, 6588873714519077015L, 2009392598285672668L, 5271098971615261612L, 1607514078628538134L, - 8433758354584418579L, 4416696933176616176L, 6747006683667534863L, 5378031953912248102L, 5397605346934027890L, - 7991774377871708805L, 8636168555094444625L, 3563466967739958280L, 6908934844075555700L, 2850773574191966624L, - 5527147875260444560L, 2280618859353573299L, 8843436600416711296L, 3648990174965717279L, 7074749280333369037L, - 1074517732601618662L, 5659799424266695229L, 6393637408194160414L, 9055679078826712367L, 4695796630997791177L, - 7244543263061369894L, 67288490056322619L, 5795634610449095915L, 1898505199416013257L, 4636507688359276732L, - 1518804159532810606L, 7418412301374842771L, 4274761062623452130L, 5934729841099874217L, 1575134442727806543L, - 4747783872879899373L, 6794130776295110719L, 7596454196607838997L, 9025934834701221989L, 6077163357286271198L, - 3531399053019067268L, 4861730685829016958L, 6514468057157164137L, 7778769097326427133L, 8578474484080507458L, - 6223015277861141707L, 1328756365151540482L, 4978412222288913365L, 6597028314234097870L, 7965459555662261385L, - 1331873265919780784L, 6372367644529809108L, 1065498612735824627L, 5097894115623847286L, 4541747704930570025L, - 8156630584998155658L, 3577447513147001717L, 6525304467998524526L, 6551306825259511697L, 5220243574398819621L, - 3396371052836654196L, 8352389719038111394L, 1744844869796736390L, 6681911775230489115L, 3240550303208344274L, - 5345529420184391292L, 2592440242566675419L, 8552847072295026067L, 5992578795477635832L, 6842277657836020854L, - 1104714221640198342L, 5473822126268816683L, 2728445784683113836L, 8758115402030106693L, 2520838848122026975L, - 7006492321624085354L, 5706019893239531903L, 5605193857299268283L, 6409490321962580684L, 8968310171678829253L, - 8410510107769173933L, 7174648137343063403L, 1194384864102473662L, 5739718509874450722L, 4644856706023889253L, - 9183549615799121156L, 53073100154402158L, 7346839692639296924L, 7421156109607342373L, 5877471754111437539L, - 7781599295056829060L, 4701977403289150031L, 8069953843416418410L, 7523163845262640050L, 9222577334724359132L, - 6018531076210112040L, 7378061867779487306L, 4814824860968089632L, 5902449494223589845L, 7703719777548943412L, - 2065221561273923105L, 6162975822039154729L, 7186200471132003969L, 4930380657631323783L, 7593634784276558337L, - 7888609052210118054L, 1081769210616762369L, 6310887241768094443L, 2710089775864365057L, 5048709793414475554L, - 5857420635433402369L, 8077935669463160887L, 3837849794580578305L, 6462348535570528709L, 8604303057777328129L, - 5169878828456422967L, 8728116853592817665L, 8271806125530276748L, 6586289336264687617L, 6617444900424221398L, - 8958380283753660417L, 5293955920339377119L, 1632681004890062849L, 8470329472543003390L, 6301638422566010881L, - 6776263578034402712L, 5041310738052808705L, 5421010862427522170L, 343699775700336641L, 8673617379884035472L, - 549919641120538625L, 6938893903907228377L, 5973958935009296385L, 5551115123125782702L, 1089818333265526785L, - 8881784197001252323L, 3588383740595798017L, 7105427357601001858L, 6560055807218548737L, 5684341886080801486L, - 8937393460516749313L, 9094947017729282379L, 1387108685230112769L, 7275957614183425903L, 2954361355555045377L, - 5820766091346740722L, 6052837899185946625L, 4656612873077392578L, 1152921504606846977L, 7450580596923828125L, 1L, - 5960464477539062500L, 1L, 4768371582031250000L, 1L, 7629394531250000000L, 1L, 6103515625000000000L, 1L, - 4882812500000000000L, 1L, 7812500000000000000L, 1L, 6250000000000000000L, 1L, 5000000000000000000L, 1L, - 8000000000000000000L, 1L, 6400000000000000000L, 1L, 5120000000000000000L, 1L, 8192000000000000000L, 1L, - 6553600000000000000L, 1L, 5242880000000000000L, 1L, 8388608000000000000L, 1L, 6710886400000000000L, 1L, - 5368709120000000000L, 1L, 8589934592000000000L, 1L, 6871947673600000000L, 1L, 5497558138880000000L, 1L, - 8796093022208000000L, 1L, 7036874417766400000L, 1L, 5629499534213120000L, 1L, 9007199254740992000L, 1L, - 7205759403792793600L, 1L, 5764607523034234880L, 1L, 4611686018427387904L, 1L, 7378697629483820646L, - 3689348814741910324L, 5902958103587056517L, 1106804644422573097L, 4722366482869645213L, 6419466937650923963L, - 7555786372591432341L, 8426472692870523179L, 6044629098073145873L, 4896503746925463381L, 4835703278458516698L, - 7606551812282281028L, 7737125245533626718L, 1102436455425918676L, 6189700196426901374L, 4571297979082645264L, - 4951760157141521099L, 5501712790637071373L, 7922816251426433759L, 3268717242906448711L, 6338253001141147007L, - 4459648201696114131L, 5070602400912917605L, 9101741783469756789L, 8112963841460668169L, 5339414816696835055L, - 6490371073168534535L, 6116206260728423206L, 5192296858534827628L, 4892965008582738565L, 8307674973655724205L, - 5984069606361426541L, 6646139978924579364L, 4787255685089141233L, 5316911983139663491L, 5674478955442268148L, - 8507059173023461586L, 5389817513965718714L, 6805647338418769269L, 2467179603801619810L, 5444517870735015415L, - 3818418090412251009L, 8711228593176024664L, 6109468944659601615L, 6968982874540819731L, 6732249563098636453L, - 5575186299632655785L, 3541125243107954001L, 8920298079412249256L, 5665800388972726402L, 7136238463529799405L, - 2687965903807225960L, 5708990770823839524L, 2150372723045780768L, 9134385233318143238L, 7129945171615159552L, - 7307508186654514591L, 169932915179262157L, 5846006549323611672L, 7514643961627230372L, 4676805239458889338L, - 2322366354559873974L, 7482888383134222941L, 1871111759924843197L, 5986310706507378352L, 8875587037423695204L, - 4789048565205902682L, 3411120815197045840L, 7662477704329444291L, 7302467711686228506L, 6129982163463555433L, - 3997299761978027643L, 4903985730770844346L, 6887188624324332438L, 7846377169233350954L, 7330152984177021577L, - 6277101735386680763L, 7708796794712572423L, 5021681388309344611L, 633014213657192454L, 8034690221294951377L, - 6546845963964373411L, 6427752177035961102L, 1548127956429588405L, 5142201741628768881L, 6772525587256536209L, - 8227522786606030210L, 7146692124868547611L, 6582018229284824168L, 5717353699894838089L, 5265614583427859334L, - 8263231774657780795L, 8424983333484574935L, 7687147617339583786L, 6739986666787659948L, 6149718093871667029L, - 5391989333430127958L, 8609123289839243947L, 8627182933488204734L, 2706550819517059345L, 6901746346790563787L, - 4009915062984602637L, 5521397077432451029L, 8741955272500547595L, 8834235323891921647L, 8453105213888010667L, - 7067388259113537318L, 3073135356368498210L, 5653910607290829854L, 6147857099836708891L, 9046256971665327767L, - 4302548137625868741L, 7237005577332262213L, 8976061732213560478L, 5789604461865809771L, 1646826163657982898L, - 4631683569492647816L, 8696158560410206965L, 7410693711188236507L, 1001132845059645012L, 5928554968950589205L, - 6334929498160581494L, 4742843975160471364L, 5067943598528465196L, 7588550360256754183L, 2574686535532678828L, - 6070840288205403346L, 5749098043168053386L, 4856672230564322677L, 2754604027163487547L, 7770675568902916283L, - 6252040850832535236L, 6216540455122333026L, 8690981495407938512L, 4973232364097866421L, 5108110788955395648L, - 7957171782556586274L, 4483628447586722714L, 6365737426045269019L, 5431577165440333333L, 5092589940836215215L, - 6189936139723221828L, 8148143905337944345L, 680525786702379117L, 6518515124270355476L, 544420629361903293L, - 5214812099416284380L, 7814234132973343281L, 8343699359066055009L, 3279402575902573442L, 6674959487252844007L, - 4468196468093013915L, 5339967589802275205L, 9108580396587276617L, 8543948143683640329L, 5350356597684866779L, - 6835158514946912263L, 6124959685518848585L, 5468126811957529810L, 8589316563156989191L, 8749002899132047697L, - 4519534464196406897L, 6999202319305638157L, 9149650793469991003L, 5599361855444510526L, 3630371820034082479L, - 8958978968711216842L, 2119246097312621643L, 7167183174968973473L, 7229420099962962799L, 5733746539975178779L, - 249512857857504755L, 9173994463960286046L, 4088569387313917931L, 7339195571168228837L, 1426181102480179183L, - 5871356456934583069L, 6674968104097008831L, 4697085165547666455L, 7184648890648562227L, 7515336264876266329L, - 2272066188182923754L, 6012269011901013063L, 3662327357917294165L, 4809815209520810450L, 6619210701075745655L, - 7695704335233296721L, 1367365084866417240L, 6156563468186637376L, 8472589697376954439L, 4925250774549309901L, - 4933397350530608390L, 7880401239278895842L, 4204086946107063100L, 6304320991423116673L, 8897292778998515965L, - 5043456793138493339L, 1583811001085947287L, 8069530869021589342L, 6223446416479425982L, 6455624695217271474L, - 1289408318441630463L, 5164499756173817179L, 2876201062124259532L, 8263199609878107486L, 8291270514140725574L, - 6610559687902485989L, 4788342003941625298L, 5288447750321988791L, 5675348010524255400L, 8461516400515182066L, - 5391208002096898316L, 6769213120412145653L, 2468291994306563491L, 5415370496329716522L, 5663982410187161116L, - 8664592794127546436L, 1683674226815637140L, 6931674235302037148L, 8725637010936330358L, 5545339388241629719L, - 1446486386636198802L, 8872543021186607550L, 6003727033359828406L, 7098034416949286040L, 4802981626687862725L, - 5678427533559428832L, 3842385301350290180L, 9085484053695086131L, 7992490889531419449L, 7268387242956068905L, - 4549318304254180398L, 5814709794364855124L, 3639454643403344318L, 4651767835491884099L, 4756238122093630616L, - 7442828536787014559L, 2075957773236943501L, 5954262829429611647L, 3505440625960509963L, 4763410263543689317L, - 8338375722881273455L, 7621456421669902908L, 5962703527126216881L, 6097165137335922326L, 8459511636442883828L, - 4877732109868737861L, 4922934901783351901L, 7804371375789980578L, 4187347028111452718L, 6243497100631984462L, - 7039226437231072498L, 4994797680505587570L, 1942032335042947675L, 7991676288808940112L, 3107251736068716280L, - 6393341031047152089L, 8019824610967838509L, 5114672824837721671L, 8260534096145225969L, 8183476519740354675L, - 304133702235675419L, 6546781215792283740L, 243306961788540335L, 5237424972633826992L, 194645569430832268L, - 8379879956214123187L, 2156107318460286790L, 6703903964971298549L, 7258909076881094917L, 5363123171977038839L, - 7651801668875831096L, 8580997075163262143L, 6708859448088464268L, 6864797660130609714L, 9056436373212681737L, - 5491838128104487771L, 9089823505941100552L, 8786941004967180435L, 1630996757909074751L, 7029552803973744348L, - 1304797406327259801L, 5623642243178995478L, 4733186739803718164L, 8997827589086392765L, 5728424376314993901L, - 7198262071269114212L, 4582739501051995121L, 5758609657015291369L, 9200214822954461581L, 9213775451224466191L, - 9186320494614273045L, 7371020360979572953L, 5504381988320463275L, 5896816288783658362L, 8092854405398280943L, - 4717453031026926690L, 2784934709576714431L, 7547924849643082704L, 4455895535322743090L, 6038339879714466163L, - 5409390835629149634L, 4830671903771572930L, 8016861483245230030L, 7729075046034516689L, 3603606336337592240L, - 6183260036827613351L, 4727559476441028954L, 4946608029462090681L, 1937373173781868001L, 7914572847139345089L, - 8633820300163854287L, 6331658277711476071L, 8751730647502038591L, 5065326622169180857L, 5156710110630675711L, - 8104522595470689372L, 872038547525260492L, 6483618076376551497L, 6231654060133073878L, 5186894461101241198L, - 1295974433364548779L, 8299031137761985917L, 228884686012322885L, 6639224910209588733L, 5717130970922723793L, - 5311379928167670986L, 8263053591480089358L, 8498207885068273579L, 308164894771456841L, 6798566308054618863L, - 2091206323188120634L, 5438853046443695090L, 5362313873292406831L, 8702164874309912144L, 8579702197267850929L, - 6961731899447929715L, 8708436165185235905L, 5569385519558343772L, 6966748932148188724L, 8911016831293350036L, - 3768100661953281312L, 7128813465034680029L, 1169806122191669888L, 5703050772027744023L, 2780519305124291072L, - 9124881235244390437L, 2604156480827910553L, 7299904988195512349L, 7617348406775193928L, 5839923990556409879L, - 7938553132791110304L, 4671939192445127903L, 8195516913603843405L, 7475102707912204646L, 2044780617540418478L, - 5980082166329763716L, 9014522123516155429L, 4784065733063810973L, 5366943291441969181L, 7654505172902097557L, - 6742434858936195528L, 6123604138321678046L, 1704599072407046100L, 4898883310657342436L, 8742376887409457526L, - 7838213297051747899L, 1075082168258445910L, 6270570637641398319L, 2704740141977711890L, 5016456510113118655L, - 4008466520953124674L, 8026330416180989848L, 6413546433524999478L, 6421064332944791878L, 8820185961561909905L, - 5136851466355833503L, 1522125547136662440L, 8218962346169333605L, 590726468047704741L, 6575169876935466884L, - 472581174438163793L, 5260135901548373507L, 2222739346921486196L, 8416217442477397611L, 5401057362445333075L, - 6732973953981918089L, 2476171482585311299L, 5386379163185534471L, 3825611593439204201L, 8618206661096855154L, - 2431629734760816398L, 6894565328877484123L, 3789978195179608280L, 5515652263101987298L, 6721331370885596947L, - 8825043620963179677L, 8909455786045999954L, 7060034896770543742L, 3438215814094889640L, 5648027917416434993L, - 8284595873388777197L, 9036844667866295990L, 2187306953196312545L, 7229475734293036792L, 1749845562557050036L, - 5783580587434429433L, 6933899672158505514L, 4626864469947543547L, 13096515613938926L, 7402983151916069675L, - 1865628832353257443L, 5922386521532855740L, 1492503065882605955L, 4737909217226284592L, 1194002452706084764L, - 7580654747562055347L, 3755078331700690783L, 6064523798049644277L, 8538085887473418112L, 4851619038439715422L, - 3141119895236824166L, 7762590461503544675L, 6870466239749873827L, 6210072369202835740L, 5496372991799899062L, - 4968057895362268592L, 4397098393439919250L, 7948892632579629747L, 8880031836874825961L, 6359114106063703798L, - 3414676654757950445L, 5087291284850963038L, 6421090138548270680L, 8139666055761540861L, 8429069814306277926L, - 6511732844609232689L, 4898581444074067179L, 5209386275687386151L, 5763539562630208905L, 8335018041099817842L, - 5532314485466423924L, 6668014432879854274L, 736502773631228816L, 5334411546303883419L, 2433876626275938215L, - 8535058474086213470L, 7583551416783411467L, 6828046779268970776L, 6066841133426729173L, 5462437423415176621L, - 3008798499370428177L, 8739899877464282594L, 1124728784250774760L, 6991919901971426075L, 2744457434771574970L, - 5593535921577140860L, 2195565947817259976L, 8949657474523425376L, 3512905516507615961L, 7159725979618740301L, - 965650005835137607L, 5727780783694992240L, 8151217634151930732L, 9164449253911987585L, 3818576177788313364L, - 7331559403129590068L, 3054860942230650691L, 5865247522503672054L, 6133237568526430876L, 4692198018002937643L, - 6751264462192099863L, 7507516828804700229L, 8957348732136404618L, 6006013463043760183L, 9010553393080078856L, - 4804810770435008147L, 1674419492351197600L, 7687697232696013035L, 4523745595132871322L, 6150157786156810428L, - 3618996476106297057L, 4920126228925448342L, 6584545995626947969L, 7872201966280717348L, 3156575963519296104L, - 6297761573024573878L, 6214609585557347207L, 5038209258419659102L, 8661036483187788089L, 8061134813471454564L, - 6478960743616640295L, 6448907850777163651L, 7027843002264267398L, 5159126280621730921L, 3777599994440458757L, - 8254602048994769474L, 2354811176362823687L, 6603681639195815579L, 3728523348461214111L, 5282945311356652463L, - 4827493086139926451L, 8452712498170643941L, 5879314530452927160L, 6762169998536515153L, 2858777216991386566L, - 5409735998829212122L, 5976370588335019576L, 8655577598126739396L, 2183495311852210675L, 6924462078501391516L, - 9125493878965589187L, 5539569662801113213L, 5455720695801516188L, 8863311460481781141L, 6884478705911470739L, - 7090649168385424913L, 3662908557358221429L, 5672519334708339930L, 6619675660628487467L, 9076030935533343889L, - 1368109020150804139L, 7260824748426675111L, 2939161623491598473L, 5808659798741340089L, 506654891422323617L, - 4646927838993072071L, 2249998320508814055L, 7435084542388915313L, 9134020534926967972L, 5948067633911132251L, - 1773193205828708893L, 4758454107128905800L, 8797252194146787761L, 7613526571406249281L, 4852231473780084609L, - 6090821257124999425L, 2037110771653112526L, 4872657005699999540L, 1629688617322490021L, 7796251209119999264L, - 2607501787715984033L, 6237000967295999411L, 3930675837543742388L, 4989600773836799529L, 1299866262664038749L, - 7983361238138879246L, 5769134835004372321L, 6386688990511103397L, 2770633460632542696L, 5109351192408882717L, - 7750529990618899641L, 8174961907854212348L, 5022150355506418780L, 6539969526283369878L, 7707069099147045347L, - 5231975621026695903L, 631632057204770793L, 8371160993642713444L, 8389308921011453915L, 6696928794914170755L, - 8556121544180118293L, 5357543035931336604L, 6844897235344094635L, 8572068857490138567L, 5417812354437685931L, - 6857655085992110854L, 644901068808238421L, 5486124068793688683L, 2360595262417545899L, 8777798510069901893L, - 1932278012497118276L, 7022238808055921514L, 5235171224739604944L, 5617791046444737211L, 6032811387162639117L, - 8988465674311579538L, 5963149404718312264L, 7190772539449263630L, 8459868338516560134L, 5752618031559410904L, - 6767894670813248108L, 9204188850495057447L, 5294608251188331487L - ) -} diff --git a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala b/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala deleted file mode 100644 index 7d0e29199..000000000 --- a/zio-json/native/src/main/scala/zio/json/internal/UnsafeNumbers.scala +++ /dev/null @@ -1,706 +0,0 @@ -/* - * Copyright 2019-2022 John A. De Goes and the ZIO Contributors - * - * 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 zio.json.internal - -import scala.util.control.NoStackTrace - -// The underlying implementation uses an exception that has no stack trace for -// the failure case, which is 20x faster than retaining stack traces. Therefore, -// we require no boxing of the results on the happy path. This slows down the -// unhappy path a little bit, but it's still on the same order of magnitude as -// the happy path. -// -// This API should only be used by people who know what they are doing. Note -// that Reader implementations consume one character beyond the number that is -// parsed, because there is no terminator character. -object UnsafeNumbers { - - // should never escape into user code - case object UnsafeNumber - extends Exception("if you see this a dev made a mistake using UnsafeNumbers") - with NoStackTrace - - def byte(num: String): Byte = - byte_(new FastStringReader(num), true) - - def byte_(in: OneCharReader, consume: Boolean): Byte = { - val n = int_(in, consume) - if (n < -128 || n > 127) throw UnsafeNumber - n.toByte - } - - def short(num: String): Short = - short_(new FastStringReader(num), true) - - def short_(in: OneCharReader, consume: Boolean): Short = { - val n = int_(in, consume) - if (n < -32768 || n > 32767) throw UnsafeNumber - n.toShort - } - - def int(num: String): Int = - int_(new FastStringReader(num), true) - - def int_(in: OneCharReader, consume: Boolean): Int = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - if (current >= '0' && current <= '9') { - var accum = '0' - current - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if ( - accum < -214748364 || { - accum = accum * 10 + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber - } - if (!consume || current == -1) { - if (negate) return accum - else if (accum != -2147483648) return -accum - } - } - throw UnsafeNumber - } - - def long(num: String): Long = - long_(new FastStringReader(num), true) - - def long_(in: OneCharReader, consume: Boolean): Long = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - if (current >= '0' && current <= '9') { - var accum = ('0' - current).toLong - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if ( - accum < -922337203685477580L || { - accum = accum * 10 + ('0' - current) - accum > 0 - } - ) throw UnsafeNumber - } - if (!consume || current == -1) { - if (negate) return accum - else if (accum != -9223372036854775808L) return -accum - } - } - throw UnsafeNumber - } - - def bigInteger(num: String, max_bits: Int): java.math.BigInteger = - bigInteger_(new FastStringReader(num), true, max_bits) - - def bigInteger_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigInteger = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - if (current >= '0' && current <= '9') { - var loM10 = (current - '0').toLong - var loDigits = 1 - var hiM10: java.math.BigDecimal = null - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - } else { - if (negate) loM10 = -loM10 - val bd = java.math.BigDecimal.valueOf(loM10) - if (hiM10 eq null) hiM10 = bd - else { - hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd) - if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber - } - loM10 = (current - '0').toLong - loDigits = 1 - } - } - if (!consume || current == -1) { - if (negate) loM10 = -loM10 - if (hiM10 eq null) return java.math.BigInteger.valueOf(loM10) - val bi = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10)).unscaledValue - if (bi.bitLength < max_bits) return bi - } - } - throw UnsafeNumber - } - - def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = - bigDecimal_(new FastStringReader(num), true, max_bits) - - def bigDecimal_(in: OneCharReader, consume: Boolean, max_bits: Int): java.math.BigDecimal = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - var loM10 = 0L - var loDigits = 0 - var hiM10: java.math.BigDecimal = null - if (current >= '0' && current <= '9') { - loM10 = (current - '0').toLong - loDigits = 1 - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) - loM10 = (current - '0').toLong - loDigits = 1 - } - } - } - var e10 = 0 - if (current == '.') { - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - e10 -= 1 - } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) - loM10 = (current - '0').toLong - loDigits = 1 - e10 -= 1 - } - } - } - if ( - loDigits != 0 && ((current | 0x20) != 'e' || { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - (current >= '0' && current <= '9') && { - var exp = '0' - current - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 - } - ) throw UnsafeNumber - } - negateExp && { - e10 += exp - e10 <= 0 - } || !negateExp && { - e10 -= exp - exp != -2147483648 - } - } - }) && (!consume || current == -1) - ) { - if (hiM10 eq null) { - if (negate) loM10 = -loM10 - return java.math.BigDecimal.valueOf(loM10, -e10) - } - return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate) - } - throw UnsafeNumber - } - - @noinline private[this] def toBigDecimal( - hi: java.math.BigDecimal, - lo: Long, - loDigits: Int, - max_bits: Int, - negate: Boolean - ): java.math.BigDecimal = { - var loM10 = lo - if (negate) loM10 = -loM10 - var hiM10 = java.math.BigDecimal.valueOf(loM10) - if (hi eq null) return hiM10 - hiM10 = hi.scaleByPowerOfTen(loDigits).add(hiM10) - if (hiM10.unscaledValue.bitLength < max_bits) return hiM10 - throw UnsafeNumber - } - - @noinline private[this] def toBigDecimal( - hi: java.math.BigDecimal, - lo: Long, - loDigits: Int, - e10: Int, - max_bits: Int, - negate: Boolean - ): java.math.BigDecimal = { - var loM10 = lo - if (negate) loM10 = -loM10 - var hiM10 = java.math.BigDecimal.valueOf(loM10, -e10) - if (hi eq null) return hiM10 - val n = loDigits.toLong + e10 - if ( - n.toInt == n && { - val scale = hi.scale - n - scale.toInt == scale - } && { - hiM10 = hi.scaleByPowerOfTen(n.toInt).add(hiM10) - hiM10.unscaledValue.bitLength < max_bits - } - ) return hiM10 - throw UnsafeNumber - } - - def float(num: String, max_bits: Int): Float = - float_(new FastStringReader(num), true, max_bits) - - def float_(in: OneCharReader, consume: Boolean, max_bits: Int): Float = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - else if (current == 'N') { - readAll(in, "aN", consume) - return Float.NaN - } - if (current == 'I' || current == '+') { - if (current == '+' && in.readChar() != 'I') throw UnsafeNumber - readAll(in, "nfinity", consume) - return if (negate) Float.NegativeInfinity else Float.PositiveInfinity - } - var loM10 = 0L - var loDigits = 0 - var hiM10: java.math.BigDecimal = null - if (current >= '0' && current <= '9') { - loM10 = (current - '0').toLong - loDigits = 1 - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) - loM10 = (current - '0').toLong - loDigits = 1 - } - } - } - var e10 = 0 - if (current == '.') { - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - e10 -= 1 - } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, max_bits, negate) - loM10 = (current - '0').toLong - loDigits = 1 - e10 -= 1 - } - } - } - if ( - loDigits != 0 && ((current | 0x20) != 'e' || { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - (current >= '0' && current <= '9') && { - var exp = '0' - current - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 - } - ) throw UnsafeNumber - } - negateExp && { - e10 += exp - e10 <= 0 - } || !negateExp && { - e10 -= exp - exp != -2147483648 - } - } - }) && (!consume || current == -1) - ) { - if (hiM10 eq null) { - var x = - if (e10 == 0) loM10.toFloat - else { - if (loM10 < 4294967296L && e10 >= loDigits - 23 && e10 <= 19 - loDigits) { - val pow10 = pow10Doubles - (if (e10 < 0) loM10 / pow10(-e10) - else loM10 * pow10(e10)).toFloat - } else toFloat(loM10, e10) - } - if (negate) x = -x - return x - } - return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).floatValue - } - throw UnsafeNumber - } - - // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical - // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct - private[this] def toFloat(m10: Long, e10: Int): Float = - if (m10 == 0 || e10 < -64) 0.0f - else if (e10 >= 39) Float.PositiveInfinity - else { - var shift = java.lang.Long.numberOfLeadingZeros(m10) - var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) - var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 - shift = java.lang.Long.numberOfLeadingZeros(m2) - m2 <<= shift - e2 -= shift - val truncatedBitNum = Math.max(-149 - e2, 40) - val savedBitNum = 64 - truncatedBitNum - val mask = -1L >>> Math.max(savedBitNum, 0) - val halfwayDiff = (m2 & mask) - (mask >>> 1) - if (Math.abs(halfwayDiff) > 1 || savedBitNum <= 0) java.lang.Float.intBitsToFloat { - var mf = 0 - if (savedBitNum > 0) mf = (m2 >>> truncatedBitNum).toInt - e2 += truncatedBitNum - if (savedBitNum >= 0 && halfwayDiff > 0) { - if (mf == 0xffffff) { - mf = 0x800000 - e2 += 1 - } else mf += 1 - } - if (e2 == -149) mf - else if (e2 >= 105) 0x7f800000 - else e2 + 150 << 23 | mf & 0x7fffff - } - else java.math.BigDecimal.valueOf(m10, -e10).floatValue - } - - def double(num: String, max_bits: Int): Double = - double_(new FastStringReader(num), true, max_bits) - - def double_(in: OneCharReader, consume: Boolean, max_bits: Int): Double = { - var current = - if (consume) in.readChar().toInt - else in.nextNonWhitespace().toInt - val negate = current == '-' - if (negate) current = in.readChar().toInt - else if (current == 'N') { - readAll(in, "aN", consume) - return Double.NaN - } - if (current == 'I' || current == '+') { - if (current == '+' && in.readChar() != 'I') throw UnsafeNumber - readAll(in, "nfinity", consume) - return if (negate) Double.NegativeInfinity else Double.PositiveInfinity - } - var loM10 = 0L - var loDigits = 0 - var hiM10: java.math.BigDecimal = null - if (current >= '0' && current <= '9') { - loM10 = (current - '0').toLong - loDigits = 1 - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = (current - '0').toLong - loDigits = 1 - } - } - } - var e10 = 0 - if (current == '.') { - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if (loM10 < 922337203685477580L) { - loM10 = loM10 * 10 + (current - '0') - loDigits += 1 - e10 -= 1 - } else { - hiM10 = toBigDecimal(hiM10, loM10, loDigits, 0, max_bits, negate) - loM10 = (current - '0').toLong - loDigits = 1 - e10 -= 1 - } - } - } - if ( - loDigits != 0 && ((current | 0x20) != 'e' || { - current = in.readChar().toInt - val negateExp = current == '-' - if (negateExp || current == '+') current = in.readChar().toInt - (current >= '0' && current <= '9') && { - var exp = '0' - current - while ({ - current = in.read() - current >= '0' && current <= '9' - }) { - if ( - exp < -214748364 || { - exp = exp * 10 + ('0' - current) - exp > 0 - } - ) throw UnsafeNumber - } - negateExp && { - e10 += exp - e10 <= 0 - } || !negateExp && { - e10 -= exp - exp != -2147483648 - } - } - }) && (!consume || current == -1) - ) { - if (hiM10 eq null) { - var x = - if (e10 == 0) loM10.toDouble - else { - if (loM10 < 4503599627370496L && e10 >= -22 && e10 <= 38 - loDigits) { - val pow10 = pow10Doubles - if (e10 < 0) loM10 / pow10(-e10) - else if (e10 <= 22) loM10 * pow10(e10) - else { - val slop = 16 - loDigits - (loM10 * pow10(slop)) * pow10(e10 - slop) - } - } else toDouble(loM10, e10) - } - if (negate) x = -x - return x - } - return toBigDecimal(hiM10, loM10, loDigits, e10, max_bits, negate).doubleValue - } - throw UnsafeNumber - } - - // Based on the 'Moderate Path' algorithm from the awesome library of Alexander Huszagh: https://github.com/Alexhuszagh/rust-lexical - // Here is his inspiring post: https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct - @inline private[this] def toDouble(m10: Long, e10: Int): Double = - if (m10 == 0 || e10 < -343) 0.0 - else if (e10 >= 310) Double.PositiveInfinity - else { - var shift = java.lang.Long.numberOfLeadingZeros(m10) - var m2 = unsignedMultiplyHigh(pow10Mantissas(e10 + 343), m10 << shift) - var e2 = (e10 * 108853 >> 15) - shift + 1 // (e10 * Math.log(10) / Math.log(2)).toInt - shift + 1 - shift = java.lang.Long.numberOfLeadingZeros(m2) - m2 <<= shift - e2 -= shift - val truncatedBitNum = Math.max(-1074 - e2, 11) - val savedBitNum = 64 - truncatedBitNum - val mask = -1L >>> Math.max(savedBitNum, 0) - val halfwayDiff = (m2 & mask) - (mask >>> 1) - if (Math.abs(halfwayDiff) > 1 || savedBitNum <= 0) java.lang.Double.longBitsToDouble { - if (savedBitNum <= 0) m2 = 0 - m2 >>>= truncatedBitNum - e2 += truncatedBitNum - if (savedBitNum >= 0 && halfwayDiff > 0) { - if (m2 == 0x1fffffffffffffL) { - m2 = 0x10000000000000L - e2 += 1 - } else m2 += 1 - } - if (e2 == -1074) m2 - else if (e2 >= 972) 0x7ff0000000000000L - else (e2 + 1075).toLong << 52 | m2 & 0xfffffffffffffL - } - else java.math.BigDecimal.valueOf(m10, -e10).doubleValue - } - - @noinline private[this] def readAll(in: OneCharReader, s: String, consume: Boolean): Unit = { - val len = s.length - var i = 0 - while (i < len) { - if (in.readChar() != s.charAt(i)) throw UnsafeNumber - i += 1 - } - val current = in.read() // to be consistent read the terminator - if (consume && current != -1 || !consume && current != '"') throw UnsafeNumber - } - - @inline private[this] def unsignedMultiplyHigh(x: Long, y: Long): Long = - Math.multiplyHigh(x, y) + x + y // FIXME: Use Math.unsignedMultiplyHigh after dropping of JDK 17 support - - private[this] final val pow10Doubles: Array[Double] = - Array(1, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, - 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22) - - private[this] final val pow10Mantissas: Array[Long] = Array( - -4671960508600951122L, -1228264617323800998L, -7685194413468457480L, -4994806998408183946L, -1631822729582842029L, - -7937418233630358124L, -5310086773610559751L, -2025922448585811785L, -8183730558007214222L, -5617977179081629873L, - -2410785455424649437L, -8424269937281487754L, -5918651403174471789L, -2786628235540701832L, -8659171674854020501L, - -6212278575140137722L, -3153662200497784248L, -8888567902952197011L, -6499023860262858360L, -3512093806901185046L, - -9112587656954322510L, -6779048552765515233L, -3862124672529506138L, -215969822234494768L, -7052510166537641086L, - -4203951689744663454L, -643253593753441413L, -7319562523736982739L, -4537767136243840520L, -1060522901877412746L, - -7580355841314464822L, -4863758783215693124L, -1468012460592228501L, -7835036815511224669L, -5182110000961642932L, - -1865951482774665761L, -8083748704375247957L, -5492999862041672042L, -2254563809124702148L, -8326631408344020699L, - -5796603242002637969L, -2634068034075909558L, -8563821548938525330L, -6093090917745768758L, -3004677628754823043L, - -8795452545612846258L, -6382629663588669919L, -3366601061058449494L, -9021654690802612790L, -6665382345075878084L, - -3720041912917459700L, -38366372719436721L, -6941508010590729807L, -4065198994811024355L, -469812725086392539L, - -7211161980820077193L, -4402266457597708587L, -891147053569747830L, -7474495936122174250L, -4731433901725329908L, - -1302606358729274481L, -7731658001846878407L, -5052886483881210105L, -1704422086424124727L, -7982792831656159810L, - -5366805021142811859L, -2096820258001126919L, -8228041688891786181L, -5673366092687344822L, -2480021597431793123L, - -8467542526035952558L, -5972742139117552794L, -2854241655469553088L, -8701430062309552536L, -6265101559459552766L, - -3219690930897053053L, -8929835859451740015L, -6550608805887287114L, -3576574988931720989L, -9152888395723407474L, - -6829424476226871438L, -3925094576856201394L, -294682202642863838L, -7101705404292871755L, -4265445736938701790L, - -720121152745989333L, -7367604748107325189L, -4597819916706768583L, -1135588877456072824L, -7627272076051127371L, - -4922404076636521310L, -1541319077368263733L, -7880853450996246689L, -5239380795317920458L, -1937539975720012668L, - -8128491512466089774L, -5548928372155224313L, -2324474446766642487L, -8370325556870233411L, -5851220927660403859L, - -2702340141148116920L, -8606491615858654931L, -6146428501395930760L, -3071349608317525546L, -8837122532839535322L, - -6434717147622031249L, -3431710416100151157L, -9062348037703676329L, -6716249028702207507L, -3783625267450371480L, - -117845565885576446L, -6991182506319567135L, -4127292114472071014L, -547429124662700864L, -7259672230555269896L, - -4462904269766699466L, -966944318780986428L, -7521869226879198374L, -4790650515171610063L, -1376627125537124675L, - -7777920981101784778L, -5110715207949843068L, -1776707991509915931L, -8027971522334779313L, -5423278384491086237L, - -2167411962186469893L, -8272161504007625539L, -5728515861582144020L, -2548958808550292121L, -8510628282985014432L, - -6026599335303880135L, -2921563150702462265L, -8743505996830120772L, -6317696477610263061L, -3285434578585440922L, - -8970925639256982432L, -6601971030643840136L, -3640777769877412266L, -9193015133814464522L, -6879582898840692749L, - -3987792605123478032L, -373054737976959636L, -7150688238876681629L, -4326674280168464132L, -796656831783192261L, - -7415439547505577019L, -4657613415954583370L, -1210330751515841308L, -7673985747338482674L, -4980796165745715438L, - -1614309188754756393L, -7926472270612804602L, -5296404319838617848L, -2008819381370884406L, -8173041140997884610L, - -5604615407819967859L, -2394083241347571919L, -8413831053483314306L, -5905602798426754978L, -2770317479606055818L, - -8648977452394866743L, -6199535797066195524L, -3137733727905356501L, -8878612607581929669L, -6486579741050024183L, - -3496538657885142324L, -9102865688819295809L, -6766896092596731857L, -3846934097318526917L, -196981603220770742L, - -7040642529654063570L, -4189117143640191558L, -624710411122851544L, -7307973034592864071L, -4523280274813692185L, - -1042414325089727327L, -7569037980822161435L, -4849611457600313890L, -1450328303573004458L, -7823984217374209643L, - -5168294253290374149L, -1848681798185579782L, -8072955151507069220L, -5479507920956448621L, -2237698882768172872L, - -8316090829371189901L, -5783427518286599473L, -2617598379430861437L, -8553528014785370254L, -6080224000054324913L, - -2988593981640518238L, -8785400266166405755L, -6370064314280619289L, -3350894374423386208L, -9011838011655698236L, - -6653111496142234891L, -3704703351750405709L, -19193171260619233L, -6929524759678968877L, -4050219931171323192L, - -451088895536766085L, -7199459587351560659L, -4387638465762062920L, -872862063775190746L, -7463067817500576073L, - -4717148753448332187L, -1284749923383027329L, -7720497729755473937L, -5038936143766954517L, -1686984161281305242L, - -7971894128441897632L, -5353181642124984136L, -2079791034228842266L, -8217398424034108273L, -5660062011615247437L, - -2463391496091671392L, -8457148712698376476L, -5959749872445582691L, -2838001322129590460L, -8691279853972075893L, - -6252413799037706963L, -3203831230369745799L, -8919923546622172981L, -6538218414850328322L, -3561087000135522498L, - -9143208402725783417L, -6817324484979841368L, -3909969587797413806L, -275775966319379353L, -7089889006590693952L, - -4250675239810979535L, -701658031336336515L, -7356065297226292178L, -4583395603105477319L, -1117558485454458744L, - -7616003081050118571L, -4908317832885260310L, -1523711272679187483L, -7869848573065574033L, -5225624697904579637L, - -1920344853953336643L, -8117744561361917258L, -5535494683275008668L, -2307682335666372931L, -8359830487432564938L, - -5838102090863318269L, -2685941595151759932L, -8596242524610931813L, -6133617137336276863L, -3055335403242958174L, - -8827113654667930715L, -6422206049907525490L, -3416071543957018958L, -9052573742614218705L, -6704031159840385477L, - -3768352931373093942L, -98755145788979524L, -6979250993759194058L, -4112377723771604669L, -528786136287117932L, - -7248020362820530564L, -4448339435098275301L, -948738275445456222L, -7510490449794491995L, -4776427043815727089L, - -1358847786342270957L, -7766808894105001205L, -5096825099203863602L, -1759345355577441598L, -8017119874876982855L, - -5409713825168840664L, -2150456263033662926L, -8261564192037121185L, -5715269221619013577L, -2532400508596379068L, - -8500279345513818773L, -6013663163464885563L, -2905392935903719049L, -8733399612580906262L, -6305063497298744923L, - -3269643353196043250L, -8961056123388608887L, -6589634135808373205L, -3625356651333078602L, -9183376934724255983L, - -6867535149977932074L, -3972732919045027189L, -354230130378896082L, -7138922859127891907L, -4311967555482476980L, - -778273425925708321L, -7403949918844649557L, -4643251380128424042L, -1192378206733142148L, -7662765406849295699L, - -4966770740134231719L, -1596777406740401745L, -7915514906853832947L, -5282707615139903279L, -1991698500497491195L, - -8162340590452013853L, -5591239719637629412L, -2377363631119648861L, -8403381297090862394L, -5892540602936190089L, - -2753989735242849707L, -8638772612167862923L, -6186779746782440750L, -3121788665050663033L, -8868646943297746252L, - -6474122660694794911L, -3480967307441105734L, -9093133594791772940L, -6754730975062328271L, -3831727700400522434L, - -177973607073265139L, -7028762532061872568L, -4174267146649952806L, -606147914885053103L, -7296371474444240046L, - -4508778324627912153L, -1024286887357502287L, -7557708332239520786L, -4835449396872013078L, -1432625727662628443L, - -7812920107430224633L, -5154464115860392887L, -1831394126398103205L, -8062150356639896359L, -5466001927372482545L, - -2220816390788215277L, -8305539271883716405L, -5770238071427257602L, -2601111570856684098L, -8543223759426509417L, - -6067343680855748868L, -2972493582642298180L, -8775337516792518219L, -6357485877563259869L, -3335171328526686933L, - -9002011107970261189L, -6640827866535438582L, -3689348814741910324L, -9223372036854775808L, -6917529027641081856L, - -4035225266123964416L, -432345564227567616L, -7187745005283311616L, -4372995238176751616L, -854558029293551616L, - -7451627795949551616L, -4702848726509551616L, -1266874889709551616L, -7709325833709551616L, -5024971273709551616L, - -1669528073709551616L, -7960984073709551616L, -5339544073709551616L, -2062744073709551616L, -8206744073709551616L, - -5646744073709551616L, -2446744073709551616L, -8446744073709551616L, -5946744073709551616L, -2821744073709551616L, - -8681119073709551616L, -6239712823709551616L, -3187955011209551616L, -8910000909647051616L, -6525815118631426616L, - -3545582879861895366L, -9133518327554766460L, -6805211891016070171L, -3894828845342699810L, -256850038250986858L, - -7078060301547948643L, -4235889358507547899L, -683175679707046970L, -7344513827457986212L, -4568956265895094861L, - -1099509313941480672L, -7604722348854507276L, -4894216917640746191L, -1506085128623544835L, -7858832233030797378L, - -5211854272861108819L, -1903131822648998119L, -8106986416796705681L, -5522047002568494197L, -2290872734783229842L, - -8349324486880600507L, -5824969590173362730L, -2669525969289315508L, -8585982758446904049L, -6120792429631242157L, - -3039304518611664792L, -8817094351773372351L, -6409681921289327535L, -3400416383184271515L, -9042789267131251553L, - -6691800565486676537L, -3753064688430957767L, -79644842111309304L, -6967307053960650171L, -4097447799023424810L, - -510123730351893109L, -7236356359111015049L, -4433759430461380907L, -930513269649338230L, -7499099821171918250L, - -4762188758037509908L, -1341049929119499481L, -7755685233340769032L, -5082920523248573386L, -1741964635633328828L, - -8006256924911912374L, -5396135137712502563L, -2133482903713240300L, -8250955842461857044L, -5702008784649933400L, - -2515824962385028846L, -8489919629131724885L, -6000713517987268202L, -2889205879056697349L, -8723282702051517699L, - -6292417359137009220L, -3253835680493873621L, -8951176327949752869L, -6577284391509803182L, -3609919470959866074L, - -9173728696990998152L, -6855474852811359786L, -3957657547586811828L, -335385916056126881L, -7127145225176161157L, - -4297245513042813542L, -759870872876129024L, -7392448323188662496L, -4628874385558440216L, -1174406963520662366L, - -7651533379841495835L, -4952730706374481889L, -1579227364540714458L, -7904546130479028392L, -5268996644671397586L, - -1974559787411859078L, -8151628894773493780L, -5577850100039479321L, -2360626606621961247L, -8392920656779807636L, - -5879464802547371641L, -2737644984756826647L, -8628557143114098510L, -6174010410465235234L, -3105826994654156138L, - -8858670899299929442L, -6461652605697523899L, -3465379738694516970L, -9083391364325154962L, -6742553186979055799L, - -3816505465296431844L, -158945813193151901L, -7016870160886801794L, -4159401682681114339L, -587566084924005019L, - -7284757830718584993L, -4494261269970843337L, -1006140569036166268L, -7546366883288685774L, -4821272585683469313L, - -1414904713676948737L, -7801844473689174817L, -5140619573684080617L, -1814088448677712867L, -8051334308064652398L, - -5452481866653427593L, -2203916314889396588L, -8294976724446954723L, -5757034887131305500L, -2584607590486743971L, - -8532908771695296838L, -6054449946191733143L, -2956376414312278525L, -8765264286586255934L, -6344894339805432014L, - -3319431906329402113L, -8992173969096958177L, -6628531442943809817L, -3673978285252374367L, -9213765455923815836L, - -6905520801477381891L, -4020214983419339459L, -413582710846786420L, -7176018221920323369L, -4358336758973016307L, - -836234930288882479L, -7440175859071633406L, -4688533805412153853L, -1248981238337804412L, -7698142301602209614L, - -5010991858575374113L, -1652053804791829737L, -7950062655635975442L, -5325892301117581398L, -2045679357969588844L, - -8196078626372074883L, -5633412264537705700L, -2430079312244744221L, -8436328597794046994L, -5933724728815170839L, - -2805469892591575644L, -8670947710510816634L, -6226998619711132888L, -3172062256211528206L, -8900067937773286985L, - -6513398903789220827L, -3530062611309138130L, -9123818159709293187L, -6793086681209228580L, -3879672333084147821L, - -237904397927796872L, -7066219276345954901L, -4221088077005055722L, -664674077828931749L, -7332950326284164199L, - -4554501889427817345L, -1081441343357383777L, -7593429867239446717L, -4880101315621920492L, -1488440626100012711L, - -7847804418953589800L, -5198069505264599346L, -1885900863153361279L, -8096217067111932656L, -5508585315462527915L, - -2274045625900771990L, -8338807543829064350L, -5811823411358942533L, -2653093245771290262L, -8575712306248138270L, - -6107954364382784934L, -3023256937051093263L, -8807064613298015146L, -6397144748195131028L, -3384744916816525881L, - -9032994600651410532L, -6679557232386875260L, -3737760522056206171L, -60514634142869810L, -6955350673980375487L, - -4082502324048081455L, -491441886632713915L, -7224680206786528053L, -4419164240055772162L, -912269281642327298L, - -7487697328667536418L, -4747935642407032618L, -1323233534581402868L, -7744549986754458649L, -5069001465015685407L, - -1724565812842218855L, -7995382660667468640L, -5382542307406947896L, -2116491865831296966L, -8240336443785642460L, - -5688734536304665171L, -2499232151953443560L, -8479549122611984081L, -5987750384837592197L, -2873001962619602342L, - -8713155254278333320L, -6279758049420528746L, -3238011543348273028L, -8941286242233752499L, -6564921784364802720L, - -3594466212028615495L, -9164070410158966541L, -6843401994271320272L, -3942566474411762436L, -316522074587315140L, - -7115355324258153819L, -4282508136895304370L, -741449152691742558L, -7380934748073420955L, -4614482416664388289L, - -1156417002403097458L, -7640289654143017767L, -4938676049251384305L, -1561659043136842477L, -7893565929601608404L, - -5255271393574622601L, -1957403223540890347L, -8140906042354138323L, -5564446534515285000L, -2343872149716718346L, - -8382449121214030822L, -5866375383090150624L, -2721283210435300376L, -8618331034163144591L, -6161227774276542835L, - -3089848699418290639L, -8848684464777513506L, -6449169562544503978L, -3449775934753242068L, -9073638986861858149L, - -6730362715149934782L, -3801267375510030573L, -139898200960150313L, -7004965403241175802L, -4144520735624081848L, - -568964901102714406L, -7273132090830278360L, -4479729095110460046L, -987975350460687153L, -7535013621679011327L, - -4807081008671376254L, -1397165242411832414L, -7790757304148477115L, -5126760611758208489L, -1796764746270372707L, - -8040506994060064798L, -5438947724147693094L, -2186998636757228463L, -8284403175614349646L, -5743817951090549153L, - -2568086420435798537L, -8522583040413455942L, -6041542782089432023L, -2940242459184402125L, -8755180564631333184L, - -6332289687361778576L, -3303676090774835316L, -8982326584375353929L, -6616222212041804507L, -3658591746624867729L, - -9204148869281624187L, -6893500068174642330L, -4005189066790915008L, -394800315061255856L, -7164279224554366766L, - -4343663012265570553L, -817892746904575288L, -7428711994456441411L, -4674203974643163860L, -1231068949876566920L, - -7686947121313936181L, -4996997883215032323L, -1634561335591402499L, -7939129862385708418L, -5312226309554747619L, - -2028596868516046619L, -8185402070463610993L, -5620066569652125837L - ) -} diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index fba88eb0e..ecbb1f877 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -300,44 +300,223 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with implicit val symbol: JsonDecoder[Symbol] = string.map(Symbol(_)) - implicit val byte: JsonDecoder[Byte] = number(Lexer.byte, _.byteValueExact) - implicit val short: JsonDecoder[Short] = number(Lexer.short, _.shortValueExact) - implicit val int: JsonDecoder[Int] = number(Lexer.int, _.intValueExact) - implicit val long: JsonDecoder[Long] = number(Lexer.long, _.longValueExact) - implicit val bigInteger: JsonDecoder[java.math.BigInteger] = number(Lexer.bigInteger, _.toBigIntegerExact) - implicit val scalaBigInt: JsonDecoder[BigInt] = number(Lexer.bigInteger, _.toBigIntegerExact) - implicit val float: JsonDecoder[Float] = number(Lexer.float, _.floatValue) - implicit val double: JsonDecoder[Double] = number(Lexer.double, _.doubleValue) - implicit val bigDecimal: JsonDecoder[java.math.BigDecimal] = number(Lexer.bigDecimal, identity) - implicit val scalaBigDecimal: JsonDecoder[BigDecimal] = - number(Lexer.bigDecimal, new BigDecimal(_, BigDecimal.defaultMathContext)) - - // numbers decode from numbers or strings for maximum compatibility - private[this] def number[A](f: (List[JsonError], RetractReader) => A, g: java.math.BigDecimal => A): JsonDecoder[A] = - new JsonDecoder[A] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = - if (in.nextNonWhitespace() != '"') { - in.retract() - f(trace, in) - } else { - val a = f(trace, in) - val c = in.readChar() - if (c != '"') Lexer.error("'\"'", c, trace) - a - } + implicit val byte: JsonDecoder[Byte] = new JsonDecoder[Byte] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Byte = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.byte(trace, in) + } else { + val a = Lexer.byte(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case Json.Num(value) => - try g(value) - catch { - case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) - } - case Json.Str(value) => f(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) - } - } + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Byte = + json match { + case Json.Num(value) => + try value.byteValueExact + catch { + case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + } + case Json.Str(value) => Lexer.byte(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } + + implicit val short: JsonDecoder[Short] = new JsonDecoder[Short] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Short = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.short(trace, in) + } else { + val a = Lexer.short(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Short = + json match { + case Json.Num(value) => + try value.shortValueExact + catch { + case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + } + case Json.Str(value) => Lexer.short(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } + + implicit val int: JsonDecoder[Int] = new JsonDecoder[Int] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Int = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.int(trace, in) + } else { + val a = Lexer.int(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Int = + json match { + case Json.Num(value) => + try value.intValueExact + catch { + case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + } + case Json.Str(value) => Lexer.int(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } + implicit val long: JsonDecoder[Long] = new JsonDecoder[Long] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Long = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.long(trace, in) + } else { + val a = Lexer.long(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Long = + json match { + case Json.Num(value) => + try value.longValueExact + catch { + case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + } + case Json.Str(value) => Lexer.long(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } + implicit val bigInteger: JsonDecoder[java.math.BigInteger] = new JsonDecoder[java.math.BigInteger] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): java.math.BigInteger = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.bigInteger(trace, in) + } else { + val a = Lexer.bigInteger(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigInteger = + json match { + case Json.Num(value) => + try value.toBigIntegerExact + catch { + case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + } + case Json.Str(value) => Lexer.bigInteger(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } + implicit val scalaBigInt: JsonDecoder[BigInt] = new JsonDecoder[BigInt] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): BigInt = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.bigInt(trace, in) + } else { + val a = Lexer.bigInt(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigInt = + json match { + case Json.Num(value) => + try BigInt(value.toBigIntegerExact) + catch { + case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + } + case Json.Str(value) => Lexer.bigInt(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } + implicit val float: JsonDecoder[Float] = new JsonDecoder[Float] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Float = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.float(trace, in) + } else { + val a = Lexer.float(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Float = + json match { + case Json.Num(value) => value.floatValue + case Json.Str(value) => Lexer.float(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } + implicit val double: JsonDecoder[Double] = new JsonDecoder[Double] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Double = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.double(trace, in) + } else { + val a = Lexer.double(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Double = + json match { + case Json.Num(value) => value.doubleValue + case Json.Str(value) => Lexer.double(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } + implicit val bigDecimal: JsonDecoder[java.math.BigDecimal] = new JsonDecoder[java.math.BigDecimal] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): java.math.BigDecimal = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.bigDecimal(trace, in) + } else { + val a = Lexer.bigDecimal(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigDecimal = + json match { + case Json.Num(value) => value + case Json.Str(value) => Lexer.bigDecimal(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } + implicit val scalaBigDecimal: JsonDecoder[BigDecimal] = new JsonDecoder[BigDecimal] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): BigDecimal = + if (in.nextNonWhitespace() != '"') { + in.retract() + Lexer.bigDecimal(trace, in) + } else { + val a = Lexer.bigDecimal(trace, in) + val c = in.readChar() + if (c != '"') Lexer.error("'\"'", c, trace) + a + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigDecimal = + json match { + case Json.Num(value) => new BigDecimal(value, BigDecimal.defaultMathContext) + case Json.Str(value) => Lexer.bigDecimal(trace, new FastStringReader(value)) + case _ => Lexer.error("expected number", trace) + } + } // Option treats empty and null values as Nothing and passes values to the decoder. // // If alternative behaviour is desired, e.g. pass null to the underlying, then diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 518ec0745..22decba0b 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -315,6 +315,15 @@ object Lexer { case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits-bit BigInteger", trace) } + def bigInt(trace: List[JsonError], in: RetractReader): BigInt = + try { + val i = UnsafeNumbers.bigInt_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits-bit BigInt", trace) + } + def float(trace: List[JsonError], in: RetractReader): Float = try { val i = UnsafeNumbers.float_(in, false, NumberMaxBits) diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index a06ec3e3a..05fbb9d81 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -145,9 +145,7 @@ object DecoderSpec extends ZIOSpecDefault { assert( "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" .fromJson[BigDecimal] - )( - isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) - ) + )(isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)"))) }, test("BigDecimal exponent too large") { assert("1.23456789012345678901e-2147483648".fromJson[BigDecimal])( @@ -175,13 +173,33 @@ object DecoderSpec extends ZIOSpecDefault { test("BigInteger too large") { assert( "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851316546851" - .fromJson[java.math.BigInteger] + .fromJson[BigInteger] )(isLeft(equalTo("(expected a 256-bit BigInteger)"))) && assert( - "17014118346046923173168730371588410572848946516548466848651357486465481896465316846" - .fromJson[java.math.BigInteger] + "17014118346046923173168730371588410572848946516548466848651357486465481896465316846".fromJson[BigInteger] )(isLeft(equalTo("(expected a 256-bit BigInteger)"))) }, + test("BigInt") { + assert("170141183460469231731687303715884105728".fromJson[BigInt])( + isRight(equalTo(BigInt("170141183460469231731687303715884105728"))) + ) && + assert("-170141183460469231731687303715884105728".fromJson[BigInt])( + isRight(equalTo(BigInt("-170141183460469231731687303715884105728"))) + ) && + assertTrue("\"Infinity\"".fromJson[BigInt].isLeft) && + assertTrue("\"+Infinity\"".fromJson[BigInt].isLeft) && + assertTrue("\"-Infinity\"".fromJson[BigInt].isLeft) && + assertTrue("\"NaN\"".fromJson[BigInt].isLeft) + }, + test("BigInt too large") { + assert( + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851316546851" + .fromJson[BigInt] + )(isLeft(equalTo("(expected a 256-bit BigInt)"))) && + assert( + "17014118346046923173168730371588410572848946516548466848651357486465481896465316846".fromJson[BigInt] + )(isLeft(equalTo("(expected a 256-bit BigInt)"))) + }, test("collections") { val arr = """[1, 2, 3]""" val obj = """{ "a": 1 }""" diff --git a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala index 4a8de6fd3..673ff71a6 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/SafeNumbersSpec.scala @@ -10,10 +10,10 @@ object SafeNumbersSpec extends ZIOSpecDefault { val spec = suite("SafeNumbers")( suite("BigDecimal")( - test("valid big decimals") { + test("valid") { check(genBigDecimal)(x => assert(SafeNumbers.bigDecimal(x.toString))(isSome(equalTo(x)))) }, - test("invalid big decimals") { + test("invalid edge cases") { val invalidBigDecimalEdgeCases = List( "N", "Inf", @@ -35,7 +35,7 @@ object SafeNumbersSpec extends ZIOSpecDefault { assert(invalidBigDecimalEdgeCases)(forall(isNone)) }, - test("valid big decimal edge cases") { + test("valid edge cases") { val invalidBigDecimalEdgeCases = List( ".0", "-.0", @@ -51,12 +51,12 @@ object SafeNumbersSpec extends ZIOSpecDefault { assert(SafeNumbers.bigDecimal(s).get.compareTo(new java.math.BigDecimal(s)))(equalTo(0)) } }, - test("invalid BigDecimal text") { + test("invalid (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.bigDecimal(s))(isNone)) } ), suite("BigInteger")( - test("valid BigInteger edge cases") { + test("valid edge cases") { val inputs = List( "0", "0123", @@ -75,31 +75,63 @@ object SafeNumbersSpec extends ZIOSpecDefault { ) } }, - test("invalid BigInteger edge cases") { + test("invalid edge cases") { val inputs = List("0e+1", "01E-1", "0.1", "", "1 ") check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInteger(s))(isNone)) }, - test("valid big Integer") { + test("valid") { check(genBigInteger)(x => assert(SafeNumbers.bigInteger(x.toString, 2048))(isSome(equalTo(x)))) }, - test("invalid BigInteger") { + test("invalid (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.bigInteger(s))(isNone)) } ), + suite("BigInt")( + test("valid edge cases") { + val inputs = List( + "0", + "0123", + "-123", + "-9223372036854775807", + "9223372036854775806", + "-9223372036854775809", + "9223372036854775808" + ) + + check(Gen.fromIterable(inputs)) { s => + assert(SafeNumbers.bigInt(s))( + isSome( + equalTo(BigInt(s)) + ) + ) + } + }, + test("invalid edge cases") { + val inputs = List("0e+1", "01E-1", "0.1", "", "1 ") + + check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInt(s))(isNone)) + }, + test("valid") { + check(genBigInteger)(x => assert(SafeNumbers.bigInt(x.toString, 2048))(isSome(equalTo(BigInt(x))))) + }, + test("invalid (text)") { + check(genAlphaLowerString)(s => assert(SafeNumbers.bigInt(s))(isNone)) + } + ), suite("Byte")( - test("valid Byte") { + test("valid") { check(Gen.byte(Byte.MinValue, Byte.MaxValue)) { x => val r = SafeNumbers.byte(x.toString) assert(r)(equalTo(ByteSome(x))) && assert(r.isEmpty)(equalTo(false)) } }, - test("invalid Byte (numbers)") { + test("invalid (numbers)") { check(Gen.int.filter(x => x < Byte.MinValue || x > Byte.MaxValue)) { x => assert(SafeNumbers.byte(x.toString))(equalTo(ByteNone)) } }, - test("invalid Byte (text)") { + test("invalid (text)") { check(genAlphaLowerString)(s => assert(SafeNumbers.byte(s).isEmpty)(equalTo(true))) }, test("ByteNone") { From a9875d6f1e964e2979588c682b82a85b2a38e7be Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 14 Feb 2025 13:06:03 +0100 Subject: [PATCH 166/311] More efficient decoding from JSON AST (#1317) --- .../src/main/scala-2.x/zio/json/macros.scala | 33 +++--- .../src/main/scala-3/zio/json/macros.scala | 28 ++--- .../src/main/scala/zio/json/JsonDecoder.scala | 100 +++++++++--------- .../main/scala/zio/json/ast/JsonType.scala | 24 ++--- .../src/main/scala/zio/json/ast/ast.scala | 36 +++---- 5 files changed, 113 insertions(+), 108 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 32c4ff86a..c170572eb 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -252,7 +252,7 @@ object DeriveJsonDecoder { if (allFieldNames.length != allFieldNames.distinct.length) { val aliasNames = aliases.map(_._1) val collisions = aliasNames - .filter(alias => names.contains(alias) || aliases.count { case (a, _) => a == alias } > 1) + .filter(alias => names.contains(alias) || aliases.count(a => a._1 == alias) > 1) .distinct val msg = s"Field names and aliases in case class ${ctx.typeName.full} must be distinct, " + s"alias(es) ${collisions.mkString(",")} collide with a field or another alias" @@ -356,16 +356,16 @@ object DeriveJsonDecoder { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(keyValues) => + case o: Json.Obj => val ps = new Array[Any](len) - for ((key, value) <- keyValues) { - namesMap.get(key) match { + o.fields.foreach { kv => + namesMap.get(kv._1) match { case Some(idx) => if (ps(idx) != null) Lexer.error("duplicate", trace) val default = defaults(idx) ps(idx) = - if ((default ne null) && (value eq Json.Null)) default() - else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, value) + if ((default ne null) && (kv._2 eq Json.Null)) default() + else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) case _ => if (no_extra) Lexer.error("invalid extra field", trace) } @@ -425,10 +425,10 @@ object DeriveJsonDecoder { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(chunk) if chunk.size == 1 => - val keyValue = chunk.head - namesMap.get(keyValue._1) match { - case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, keyValue._2).asInstanceOf[A] + case o: Json.Obj if o.fields.length == 1 => + val kv = o.fields(0) + namesMap.get(kv._1) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2).asInstanceOf[A] case _ => Lexer.error("invalid disambiguator", trace) } case _ => Lexer.error("expected single field object", trace) @@ -459,9 +459,12 @@ object DeriveJsonDecoder { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(fields) => - fields.find { case (key, _) => key == hintfield } match { - case Some((_, Json.Str(name))) => + case o: Json.Obj => + o.fields.collectFirst { + case kv if kv._1 == hintfield && kv._2.isInstanceOf[Json.Str] => + kv._2.asInstanceOf[Json.Str].value + } match { + case Some(name) => namesMap.get(name) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] case _ => Lexer.error("invalid disambiguator", trace) @@ -628,8 +631,8 @@ object DeriveJsonEncoder { override def toJsonAST(a: A): Either[String, Json] = ctx.split(a) { sub => sub.typeclass.toJsonAST(sub.cast(a)).flatMap { - case Json.Obj(fields) => - new Right(Json.Obj((hintfield -> Json.Str(names(sub.index))) +: fields)) // hint field is always first + case o: Json.Obj => + new Right(Json.Obj((hintfield -> Json.Str(names(sub.index))) +: o.fields)) // hint field is always first case _ => new Left("expected object") } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index aadc1c31f..c220a800b 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -362,16 +362,16 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(keyValues) => + case o: Json.Obj => val ps = new Array[Any](len) - for ((key, value) <- keyValues) { - namesMap.get(key) match { + o.fields.foreach { kv => + namesMap.get(kv._1) match { case Some(idx) => if (ps(idx) != null) Lexer.error("duplicate", trace) val default = defaults(idx) ps(idx) = - if ((default ne null) && (value eq Json.Null)) default() - else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, value) + if ((default ne null) && (kv._2 eq Json.Null)) default() + else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) case _ => if (no_extra) Lexer.error("invalid extra field", trace) } @@ -427,7 +427,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Str(typeName) => namesMap.get(typeName) match { + case s: Json.Str => namesMap.get(s.value) match { case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) case _ => Lexer.error("invalid enumeration value", trace) } @@ -453,8 +453,8 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(chunk) if chunk.size == 1 => - val keyValue = chunk.head + case o: Json.Obj if o.fields.length == 1 => + val keyValue = o.fields(0) namesMap.get(keyValue._1) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, keyValue._2).asInstanceOf[A] case _ => Lexer.error("invalid disambiguator", trace) @@ -487,9 +487,11 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Obj(fields) => - fields.find { case (key, _) => key == hintfield } match { - case Some((_, Json.Str(name))) => + case o: Json.Obj => + o.fields.collectFirst { case kv if kv._1 == hintfield && kv._2.isInstanceOf[Json.Str] => + kv._2.asInstanceOf[Json.Str].value + } match { + case Some(name) => namesMap.get(name) match { case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] case _ => Lexer.error("invalid disambiguator", trace) @@ -705,11 +707,11 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv override final def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => sub.typeclass.toJsonAST(sub.cast(a)).flatMap { - case Json.Obj(fields) => + case o: Json.Obj => val name = sub.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - new Right(new Json.Obj((hintField -> new Json.Str(name)) +: fields)) // hint field is always first + new Right(Json.Obj((hintField -> new Json.Str(name)) +: o.fields)) // hint field is always first case _ => new Left("expected object") } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index ecbb1f877..da2878218 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -273,8 +273,8 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): String = json match { - case Json.Str(value) => value - case _ => Lexer.error("expected string", trace) + case s: Json.Str => s.value + case _ => Lexer.error("expected string", trace) } } @@ -283,8 +283,8 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Boolean = json match { - case Json.Bool(value) => value - case _ => Lexer.error("expected boolean", trace) + case b: Json.Bool => b.value + case _ => Lexer.error("expected boolean", trace) } } @@ -293,8 +293,8 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Char = json match { - case Json.Str(s) if s.length == 1 => s.charAt(0) - case _ => Lexer.error("expected single character string", trace) + case s: Json.Str if s.value.length == 1 => s.value.charAt(0) + case _ => Lexer.error("expected single character string", trace) } } @@ -314,13 +314,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Byte = json match { - case Json.Num(value) => - try value.byteValueExact + case n: Json.Num => + try n.value.byteValueExact catch { case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) } - case Json.Str(value) => Lexer.byte(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case s: Json.Str => Lexer.byte(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } @@ -338,13 +338,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Short = json match { - case Json.Num(value) => - try value.shortValueExact + case n: Json.Num => + try n.value.shortValueExact catch { case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) } - case Json.Str(value) => Lexer.short(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case s: Json.Str => Lexer.short(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } @@ -362,13 +362,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Int = json match { - case Json.Num(value) => - try value.intValueExact + case n: Json.Num => + try n.value.intValueExact catch { case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) } - case Json.Str(value) => Lexer.int(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case s: Json.Str => Lexer.int(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } implicit val long: JsonDecoder[Long] = new JsonDecoder[Long] { @@ -385,13 +385,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Long = json match { - case Json.Num(value) => - try value.longValueExact + case n: Json.Num => + try n.value.longValueExact catch { case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) } - case Json.Str(value) => Lexer.long(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case s: Json.Str => Lexer.long(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } @@ -409,13 +409,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigInteger = json match { - case Json.Num(value) => - try value.toBigIntegerExact + case n: Json.Num => + try n.value.toBigIntegerExact catch { case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) } - case Json.Str(value) => Lexer.bigInteger(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case s: Json.Str => Lexer.bigInteger(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } implicit val scalaBigInt: JsonDecoder[BigInt] = new JsonDecoder[BigInt] { @@ -432,13 +432,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigInt = json match { - case Json.Num(value) => - try BigInt(value.toBigIntegerExact) + case n: Json.Num => + try BigInt(n.value.toBigIntegerExact) catch { case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) } - case Json.Str(value) => Lexer.bigInt(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case s: Json.Str => Lexer.bigInt(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } implicit val float: JsonDecoder[Float] = new JsonDecoder[Float] { @@ -455,9 +455,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Float = json match { - case Json.Num(value) => value.floatValue - case Json.Str(value) => Lexer.float(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case n: Json.Num => n.value.floatValue + case s: Json.Str => Lexer.float(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } implicit val double: JsonDecoder[Double] = new JsonDecoder[Double] { @@ -474,9 +474,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Double = json match { - case Json.Num(value) => value.doubleValue - case Json.Str(value) => Lexer.double(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case n: Json.Num => n.value.doubleValue + case s: Json.Str => Lexer.double(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } implicit val bigDecimal: JsonDecoder[java.math.BigDecimal] = new JsonDecoder[java.math.BigDecimal] { @@ -493,9 +493,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigDecimal = json match { - case Json.Num(value) => value - case Json.Str(value) => Lexer.bigDecimal(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case n: Json.Num => n.value + case s: Json.Str => Lexer.bigDecimal(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } implicit val scalaBigDecimal: JsonDecoder[BigDecimal] = new JsonDecoder[BigDecimal] { @@ -512,9 +512,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigDecimal = json match { - case Json.Num(value) => new BigDecimal(value, BigDecimal.defaultMathContext) - case Json.Str(value) => Lexer.bigDecimal(trace, new FastStringReader(value)) - case _ => Lexer.error("expected number", trace) + case n: Json.Num => new BigDecimal(n.value, BigDecimal.defaultMathContext) + case s: Json.Str => Lexer.bigDecimal(trace, new FastStringReader(s.value)) + case _ => Lexer.error("expected number", trace) } } // Option treats empty and null values as Nothing and passes values to the decoder. @@ -692,8 +692,8 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = json match { - case Json.Arr(elements) => - elements.map { + case a: Json.Arr => + a.elements.map { var i = 0 json => val span = new JsonError.ArrayAccess(i) @@ -884,8 +884,8 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { - case Json.Str(value) => parseJavaTime(trace, value) - case _ => Lexer.error("expected string", trace) + case s: Json.Str => parseJavaTime(trace, s.value) + case _ => Lexer.error("expected string", trace) } // Commonized handling for decoding from string to java.time Class @@ -915,8 +915,8 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID = json match { - case Json.Str(value) => parseUUID(trace, value) - case _ => Lexer.error("expected string", trace) + case s: Json.Str => parseUUID(trace, s.value) + case _ => Lexer.error("expected string", trace) } @inline private[this] def parseUUID(trace: List[JsonError], s: String): UUID = @@ -932,8 +932,8 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.util.Currency = json match { - case Json.Str(value) => parseCurrency(trace, value) - case _ => Lexer.error("expected string", trace) + case s: Json.Str => parseCurrency(trace, s.value) + case _ => Lexer.error("expected string", trace) } @inline private[this] def parseCurrency(trace: List[JsonError], s: String): java.util.Currency = diff --git a/zio-json/shared/src/main/scala/zio/json/ast/JsonType.scala b/zio-json/shared/src/main/scala/zio/json/ast/JsonType.scala index 7d26596a5..42b240435 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/JsonType.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/JsonType.scala @@ -23,48 +23,48 @@ object JsonType { case object Null extends JsonType[Json.Null] { def get(json: Json): Either[String, Json.Null] = json match { - case Json.Null => Right(Json.Null) - case _ => Left("expected null") + case _: Json.Null.type => new Right(Json.Null) + case _ => new Left("expected null") } } case object Bool extends JsonType[Json.Bool] { def get(json: Json): Either[String, Json.Bool] = json match { - case x @ Json.Bool(_) => Right(x) - case _ => Left("expected boolean") + case x: Json.Bool => new Right(x) + case _ => new Left("expected boolean") } } case object Obj extends JsonType[Json.Obj] { def get(json: Json): Either[String, Json.Obj] = json match { - case x @ Json.Obj(_) => Right(x) - case _ => Left("expected object") + case x: Json.Obj => new Right(x) + case _ => new Left("expected object") } } case object Arr extends JsonType[Json.Arr] { def get(json: Json): Either[String, Json.Arr] = json match { - case x @ Json.Arr(_) => Right(x) - case _ => Left("expected array") + case x: Json.Arr => new Right(x) + case _ => new Left("expected array") } } case object Str extends JsonType[Json.Str] { def get(json: Json): Either[String, Json.Str] = json match { - case x @ Json.Str(_) => Right(x) - case _ => Left("expected string") + case x: Json.Str => new Right(x) + case _ => new Left("expected string") } } case object Num extends JsonType[Json.Num] { def get(json: Json): Either[String, Json.Num] = json match { - case x @ Json.Num(_) => Right(x) - case _ => Left("expected number") + case x: Json.Num => new Right(x) + case _ => new Left("expected number") } } } diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index 9de0e145c..5f4ed7c90 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -59,7 +59,7 @@ sealed abstract class Json { self => jsonArray: Chunk[Json] => X, jsonObject: Json.Obj => X ): X = self match { - case Json.Arr(a) => jsonArray(a) + case a: Json.Arr => jsonArray(a.elements) case o: Json.Obj => jsonObject(o) case _ => or } @@ -149,15 +149,15 @@ sealed abstract class Json { self => case JsonCursor.DownField(parent, field) => self.get(parent).flatMap { case Obj(fields) => - fields.collectFirst { case (key, value) if key == field => Right(value) } match { + fields.collectFirst { case kv if kv._1 == field => Right(kv._2) } match { case Some(x) => x case None => Left(s"No such field: '$field'") } } case JsonCursor.DownElement(parent, index) => - self.get(parent).flatMap { case Arr(elements) => - elements.lift(index).map(Right(_)).getOrElse(Left(s"The array does not have index ${index}")) + self.get(parent).flatMap { case a: Arr => + a.elements.lift(index).map(Right(_)).getOrElse(Left(s"The array does not have index ${index}")) } case JsonCursor.FilterType(parent, t @ jsonType) => @@ -167,22 +167,22 @@ sealed abstract class Json { self => override final def hashCode: Int = 31 * { self match { - case Obj(fields) => + case s: Str => s.value.hashCode + case n: Num => n.value.hashCode + case b: Bool => b.value.hashCode + case o: Obj => var result = 0 - fields.foreach(tuple => result = result ^ tuple.hashCode) + o.fields.foreach(tuple => result = result ^ tuple.hashCode) result - case Arr(elements) => + case a: Arr => var result = 0 var index = 0 - elements.foreach { json => + a.elements.foreach { json => result = result ^ (index, json).hashCode index += 1 } result - case Bool(value) => value.hashCode - case Str(value) => value.hashCode - case Num(value) => value.hashCode - case Json.Null => 1 + case _ => 1 } } @@ -250,9 +250,9 @@ sealed abstract class Json { self => final def transformDown(f: Json => Json): Json = { def loop(json: Json): Json = f(json) match { - case Obj(fields) => Obj(fields.map { case (name, value) => (name, loop(value)) }) - case Arr(elements) => Arr(elements.map(loop(_))) - case json => json + case o: Obj => Obj(o.fields.map(kv => (kv._1, loop(kv._2)))) + case a: Arr => Arr(a.elements.map(loop(_))) + case json => json } loop(self) @@ -302,9 +302,9 @@ sealed abstract class Json { self => final def transformUp(f: Json => Json): Json = { def loop(json: Json): Json = json match { - case Obj(fields) => f(Obj(fields.map { case (name, value) => (name, loop(value)) })) - case Arr(elements) => f(Arr(elements.map(loop(_)))) - case json => f(json) + case o: Obj => f(Obj(o.fields.map(kv => (kv._1, loop(kv._2))))) + case a: Arr => f(Arr(a.elements.map(loop(_)))) + case json => f(json) } loop(self) From 457d55637a21d0c9f432f5973928bfc756b40d46 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:17:05 +0100 Subject: [PATCH 167/311] Update snakeyaml to 2.4 (#1318) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 01e9d4423..1cb6ce740 100644 --- a/build.sbt +++ b/build.sbt @@ -277,7 +277,7 @@ lazy val zioJsonYaml = project .settings(buildInfoSettings("zio.json.yaml")) .settings( libraryDependencies ++= Seq( - "org.yaml" % "snakeyaml" % "2.3", + "org.yaml" % "snakeyaml" % "2.4", "org.scala-lang.modules" %% "scala-collection-compat" % "2.13.0", "dev.zio" %% "zio" % zioVersion, "dev.zio" %% "zio-test" % zioVersion % "test", From d9786843031f32d1eca6f4b04db63193746fd064 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 17 Feb 2025 14:36:09 +0100 Subject: [PATCH 168/311] More efficient encoding of floats and doubles (#1319) --- .../scala/zio/json/internal/SafeNumbers.scala | 139 +++++++++++++----- .../scala/zio/json/internal/SafeNumbers.scala | 132 ++++++++++++----- 2 files changed, 196 insertions(+), 75 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 60dd881aa..2e833fa03 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -178,15 +178,9 @@ object SafeNumbers { exp += len - 1 if (exp < -3 || exp >= 7) { val sdv = stripTrailingZeros(dv) - if (sdv < 10) out.write((sdv.toInt | '0').toChar, '.', '0', 'E') - else { - val w = writes.get - write(sdv, w) - val cs = w.getChars - out.write(cs(0), '.') - out.write(cs, 1, w.length) - out.write('E') - } + writeMantissaWithDot(sdv, out) + if (sdv >= 10) out.write('E') + else out.write('0', 'E') write(exp, out) } else if (exp < 0) { out.write('0', '.') @@ -194,18 +188,18 @@ object SafeNumbers { exp += 1 exp != 0 }) out.write('0') - write(stripTrailingZeros(dv), out) + writeMantissa(stripTrailingZeros(dv), out) } else { exp += 1 if (exp < len) { val w = writes.get - write(stripTrailingZeros(dv), w) + writeMantissa(stripTrailingZeros(dv), w) val cs = w.getChars out.write(cs, 0, exp) out.write('.') out.write(cs, exp, w.length) } else { - write(dv.toInt, out) + writeMantissa(dv.toInt, out) out.write('.', '0') } } @@ -280,15 +274,9 @@ object SafeNumbers { exp += len - 1 if (exp < -3 || exp >= 7) { val sdv = stripTrailingZeros(dv) - if (sdv < 10) out.write((sdv | '0').toChar, '.', '0', 'E') - else { - val w = writes.get - write(sdv, w) - val cs = w.getChars - out.write(cs(0), '.') - out.write(cs, 1, w.length) - out.write('E') - } + writeMantissaWithDot(sdv, out) + if (sdv >= 10) out.write('E') + else out.write('0', 'E') write(exp, out) } else if (exp < 0) { out.write('0', '.') @@ -296,18 +284,18 @@ object SafeNumbers { exp += 1 exp != 0 }) out.write('0') - write(stripTrailingZeros(dv), out) + writeMantissa(stripTrailingZeros(dv), out) } else { exp += 1 if (exp < len) { val w = writes.get - write(stripTrailingZeros(dv), w) + writeMantissa(stripTrailingZeros(dv), w) val cs = w.getChars out.write(cs, 0, exp) out.write('.') out.write(cs, exp, w.length) } else { - write(dv, out) + writeMantissa(dv, out) out.write('.', '0') } } @@ -379,16 +367,16 @@ object SafeNumbers { } @inline private[this] def stripTrailingZeros(x: Long): Long = { - var q0 = x.toInt + var q0, q1 = x if ( - q0 == x || { - q0 = ((x >>> 8) * 2.56e-6).toInt // divide a medium positive long by 100000000 + (q1 << 56 == 0L) && { + q0 = ((q1 >>> 8) * 2.56e-6).toLong // divide a medium positive long by 100000000 q0 * 100000000L == x } - ) return stripTrailingZeros(q0).toLong - var q1, y, z = x - var r1 = 0 + ) return stripTrailingZeros(q0.toInt).toLong while ({ + q0 = q1 + var z = q1 q1 = (q1 >>> 1) + (q1 >>> 2) // Based upon the divu10() code from Hacker's Delight 2nd Edition by Henry Warren q1 += q1 >>> 4 q1 += q1 >>> 8 @@ -396,25 +384,23 @@ object SafeNumbers { q1 += q1 >>> 32 z -= q1 & 0xfffffffffffffff8L q1 >>>= 3 - r1 = (z - (q1 << 1)).toInt + var r1 = (z - (q1 << 1)).toInt if (r1 >= 10) { q1 += 1L r1 -= 10 } r1 == 0 - }) { - y = q1 - z = q1 - } - y + }) () + q0 } @inline private[this] def stripTrailingZeros(x: Int): Int = { var q0, q1 = x while ({ + q0 = q1 q1 /= 10 q1 * 10 == q0 // check if q is divisible by 100 - }) q0 = q1 + }) () q0 } @@ -461,8 +447,23 @@ object SafeNumbers { } } - @inline def write(a: Int, out: Write): Unit = { - val ds = digits + @inline private[this] def writeMantissa(q0: Long, out: Write): Unit = + if (q0.toInt == q0) writeMantissa(q0.toInt, out) + else { + val q1 = ((q0 >>> 8) * 2.56e-6).toLong // divide a medium positive long by 100000000 + writeMantissa(q1.toInt, out) + write8Digits((q0 - q1 * 100000000L).toInt, out) + } + + @inline private[this] def writeMantissaWithDot(q0: Long, out: Write): Unit = + if (q0.toInt == q0) writeMantissaWithDot(q0.toInt, out) + else { + val q1 = ((q0 >>> 8) * 2.56e-6).toLong // divide a medium positive long by 100000000 + writeMantissaWithDot(q1.toInt, out) + write8Digits((q0 - q1 * 100000000L).toInt, out) + } + + def write(a: Int, out: Write): Unit = { var q0 = a if (q0 < 0) { q0 = -q0 @@ -472,6 +473,11 @@ object SafeNumbers { q0 = 147483648 } } + writeMantissa(q0, out) + } + + private[this] def writeMantissa(q0: Int, out: Write): Unit = { + val ds = digits if (q0 < 100) { if (q0 < 10) out.write((q0 | '0').toChar) else out.write(ds(q0)) @@ -509,7 +515,60 @@ object SafeNumbers { } } - @inline private[this] def write8Digits(x: Int, out: Write): Unit = { + private[this] def writeMantissaWithDot(q0: Int, out: Write): Unit = { + val ds = digits + if (q0 < 100) { + if (q0 < 10) out.write((q0 | '0').toChar, '.') + else { + val d1 = ds(q0) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + } else if (q0 < 10000) { + val q1 = q0 * 5243 >> 19 // divide a small positive int by 100 + val d2 = ds(q0 - q1 * 100) + if (q0 < 1000) out.write((q1 | '0').toChar, '.') + else { + val d1 = ds(q1) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + out.write(d2) + } else if (q0 < 1000000) { + val q1 = q0 / 100 + val r1 = q0 - q1 * 100 + val q2 = q1 * 5243 >> 19 // divide a small positive int by 100 + val r2 = q1 - q2 * 100 + if (q0 < 100000) out.write((q2 | '0').toChar, '.') + else { + val d1 = ds(q2) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + out.write(ds(r2), ds(r1)) + } else if (q0 < 100000000) { + val q1 = q0 / 100 + val r1 = q0 - q1 * 100 + val q2 = q1 / 100 + val r2 = q1 - q2 * 100 + val q3 = q2 * 5243 >> 19 // divide a small positive int by 100 + val r3 = q2 - q3 * 100 + if (q0 < 10000000) out.write((q3 | '0').toChar, '.') + else { + val d1 = ds(q3) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + out.write(ds(r3), ds(r2), ds(r1)) + } else { + val q1 = q0 / 100000000 + val r1 = q0 - q1 * 100000000 + if (q0 < 1000000000) out.write((q1 | '0').toChar, '.') + else { + val d1 = ds(q1) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + write8Digits(r1, out) + } + } + + private[this] def write8Digits(x: Int, out: Write): Unit = { val ds = digits val q1 = x / 10000 val q2 = q1 * 5243 >> 19 // divide a small positive int by 100 diff --git a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala index f278fc1c6..506be9bcc 100644 --- a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -169,15 +169,9 @@ object SafeNumbers { exp += len - 1 if (exp < -3 || exp >= 7) { val sdv = stripTrailingZeros(dv) - if (sdv < 10) out.write((sdv.toInt | '0').toChar, '.', '0', 'E') - else { - val w = writes.get - write(sdv, w) - val cs = w.getChars - out.write(cs(0), '.') - out.write(cs, 1, w.length) - out.write('E') - } + writeMantissaWithDot(sdv, out) + if (sdv >= 10) out.write('E') + else out.write('0', 'E') write(exp, out) } else if (exp < 0) { out.write('0', '.') @@ -185,18 +179,18 @@ object SafeNumbers { exp += 1 exp != 0 }) out.write('0') - write(stripTrailingZeros(dv), out) + writeMantissa(stripTrailingZeros(dv), out) } else { exp += 1 if (exp < len) { val w = writes.get - write(stripTrailingZeros(dv), w) + writeMantissa(stripTrailingZeros(dv), w) val cs = w.getChars out.write(cs, 0, exp) out.write('.') out.write(cs, exp, w.length) } else { - write(dv.toInt, out) + writeMantissa(dv.toInt, out) out.write('.', '0') } } @@ -271,15 +265,9 @@ object SafeNumbers { exp += len - 1 if (exp < -3 || exp >= 7) { val sdv = stripTrailingZeros(dv) - if (sdv < 10) out.write((sdv | '0').toChar, '.', '0', 'E') - else { - val w = writes.get - write(sdv, w) - val cs = w.getChars - out.write(cs(0), '.') - out.write(cs, 1, w.length) - out.write('E') - } + writeMantissaWithDot(sdv, out) + if (sdv >= 10) out.write('E') + else out.write('0', 'E') write(exp, out) } else if (exp < 0) { out.write('0', '.') @@ -287,18 +275,18 @@ object SafeNumbers { exp += 1 exp != 0 }) out.write('0') - write(stripTrailingZeros(dv), out) + writeMantissa(stripTrailingZeros(dv), out) } else { exp += 1 if (exp < len) { val w = writes.get - write(stripTrailingZeros(dv), w) + writeMantissa(stripTrailingZeros(dv), w) val cs = w.getChars out.write(cs, 0, exp) out.write('.') out.write(cs, exp, w.length) } else { - write(dv, out) + writeMantissa(dv, out) out.write('.', '0') } } @@ -357,28 +345,29 @@ object SafeNumbers { } private[this] def stripTrailingZeros(x: Long): Long = { - var q0 = x.toInt + var q0, q1 = x if ( - q0 == x || { - q0 = (Math.multiplyHigh(x, 6189700196426901375L) >>> 25).toInt // divide a positive long by 100000000 - (x - q0 * 100000000L).toInt == 0 + (q1 << 56 == 0L) && { + q0 = Math.multiplyHigh(q1, 6189700196426901375L) >>> 25 // divide a positive long by 100000000 + x - q0 * 100000000L == 0L } - ) return stripTrailingZeros(q0).toLong - var y, q1 = x + ) return stripTrailingZeros(q0.toInt).toLong while ({ + q0 = q1 q1 = Math.multiplyHigh(q1, 1844674407370955168L) // divide a positive long by 10 - q1 * 10 == y - }) y = q1 - y + q1 * 10 == q0 + }) () + q0 } private[this] def stripTrailingZeros(x: Int): Int = { var q0, q1 = x while ({ val qp = q1 * 3435973837L + q0 = q1 q1 = (qp >> 35).toInt // divide a positive int by 10 (qp & 0x7e0000000L) == 0 // check if q is divisible by 10 - }) q0 = q1 + }) () q0 } @@ -407,8 +396,23 @@ object SafeNumbers { } } + private[this] def writeMantissa(q0: Long, out: Write): Unit = + if (q0.toInt == q0) writeMantissa(q0.toInt, out) + else { + val q1 = Math.multiplyHigh(q0, 6189700196426901375L) >>> 25 // divide a positive long by 100000000 + writeMantissa(q1.toInt, out) + write8Digits((q0 - q1 * 100000000L).toInt, out) + } + + private[this] def writeMantissaWithDot(q0: Long, out: Write): Unit = + if (q0.toInt == q0) writeMantissaWithDot(q0.toInt, out) + else { + val q1 = Math.multiplyHigh(q0, 6189700196426901375L) >>> 25 // divide a positive long by 100000000 + writeMantissaWithDot(q1.toInt, out) + write8Digits((q0 - q1 * 100000000L).toInt, out) + } + def write(a: Int, out: Write): Unit = { - val ds = digits var q0 = a if (q0 < 0) { q0 = -q0 @@ -418,6 +422,11 @@ object SafeNumbers { q0 = 147483648 } } + writeMantissa(q0, out) + } + + private[this] def writeMantissa(q0: Int, out: Write): Unit = { + val ds = digits if (q0 < 100) { // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ if (q0 < 10) out.write((q0 | '0').toChar) else out.write(ds(q0)) @@ -454,6 +463,59 @@ object SafeNumbers { } } + private[this] def writeMantissaWithDot(q0: Int, out: Write): Unit = { + val ds = digits + if (q0 < 100) { // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ + if (q0 < 10) out.write((q0 | '0').toChar, '.') + else { + val d1 = ds(q0) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + } else if (q0 < 10000) { + val q1 = q0 * 5243 >> 19 // divide a small positive int by 100 + val d2 = ds(q0 - q1 * 100) + if (q0 < 1000) out.write((q1 | '0').toChar, '.') + else { + val d1 = ds(q1) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + out.write(d2) + } else if (q0 < 1000000) { + val y1 = q0 * 429497L + val y2 = (y1 & 0xffffffffL) * 100 + val y3 = (y2 & 0xffffffffL) * 100 + if (q0 < 100000) out.write(((y1 >> 32).toInt | '0').toChar, '.') + else { + val d1 = ds((y1 >> 32).toInt) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + out.write(ds((y2 >> 32).toInt), ds((y3 >> 32).toInt)) + } else if (q0 < 100000000) { + val y1 = q0 * 140737489L + val y2 = (y1 & 0x7fffffffffffL) * 100 + val y3 = (y2 & 0x7fffffffffffL) * 100 + val y4 = (y3 & 0x7fffffffffffL) * 100 + if (q0 < 10000000) out.write(((y1 >> 47).toInt | '0').toChar, '.') + else { + val d1 = ds((y1 >> 47).toInt) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + out.write(ds((y2 >> 47).toInt), ds((y3 >> 47).toInt), ds((y4 >> 47).toInt)) + } else { + val y1 = q0 * 1441151881L + val y2 = (y1 & 0x1ffffffffffffffL) * 100 + val y3 = (y2 & 0x1ffffffffffffffL) * 100 + val y4 = (y3 & 0x1ffffffffffffffL) * 100 + val y5 = (y4 & 0x1ffffffffffffffL) * 100 + if (q0 < 1000000000) out.write(((y1 >>> 57).toInt | '0').toChar, '.') + else { + val d1 = ds((y1 >>> 57).toInt) + out.write((d1 & 0xff).toChar, '.', (d1 >> 8).toChar) + } + out.write(ds((y2 >>> 57).toInt), ds((y3 >>> 57).toInt), ds((y4 >>> 57).toInt), ds((y5 >>> 57).toInt)) + } + } + private[this] def write8Digits(x: Int, out: Write): Unit = { val ds = digits // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ val y1 = x * 140737489L From f44c01d472e3dcc630901c329544b4138571bd97 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 17 Feb 2025 22:19:29 +0100 Subject: [PATCH 169/311] More efficient encoding of floats and doubles without an exponent (#1320) --- .../scala/zio/json/internal/SafeNumbers.scala | 54 ++++++++++++------- .../scala/zio/json/internal/SafeNumbers.scala | 52 ++++++++++++------ 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 2e833fa03..9e76042e8 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -190,14 +190,19 @@ object SafeNumbers { }) out.write('0') writeMantissa(stripTrailingZeros(dv), out) } else { - exp += 1 - if (exp < len) { - val w = writes.get - writeMantissa(stripTrailingZeros(dv), w) - val cs = w.getChars - out.write(cs, 0, exp) + var pow10i = len - exp - 1 + if (pow10i > 0) { + val pow10 = pow10longs(pow10i) + val q = dv / pow10 + val r = dv - q * pow10 + writeMantissa(q, out) out.write('.') - out.write(cs, exp, w.length) + pow10i -= digitCount(r) + while (pow10i > 0) { + out.write('0') + pow10i -= 1 + } + writeMantissa(stripTrailingZeros(r), out) } else { writeMantissa(dv.toInt, out) out.write('.', '0') @@ -286,14 +291,19 @@ object SafeNumbers { }) out.write('0') writeMantissa(stripTrailingZeros(dv), out) } else { - exp += 1 - if (exp < len) { - val w = writes.get - writeMantissa(stripTrailingZeros(dv), w) - val cs = w.getChars - out.write(cs, 0, exp) + var pow10i = len - exp - 1 + if (pow10i > 0) { + val pow10 = pow10ints(pow10i) + val q = dv / pow10 + val r = dv - q * pow10 + writeMantissa(q, out) out.write('.') - out.write(cs, exp, w.length) + pow10i -= digitCount(r) + while (pow10i > 0) { + out.write('0') + pow10i -= 1 + } + writeMantissa(stripTrailingZeros(r), out) } else { writeMantissa(dv, out) out.write('.', '0') @@ -394,7 +404,7 @@ object SafeNumbers { q0 } - @inline private[this] def stripTrailingZeros(x: Int): Int = { + private[this] def stripTrailingZeros(x: Int): Int = { var q0, q1 = x while ({ q0 = q1 @@ -415,7 +425,7 @@ object SafeNumbers { } } var q = q0.toInt - if (q0 == q) write(q, out) + if (q0 == q) writeMantissa(q, out) else { var last: Char = 0 if (q0 >= 1000000000000000000L) { @@ -436,10 +446,10 @@ object SafeNumbers { } val q1 = ((q0 >>> 8) * 2.56e-6).toLong // divide a medium positive long by 100000000 q = q1.toInt - if (q1 == q) write(q, out) + if (q1 == q) writeMantissa(q, out) else { q = ((q1 >>> 8) * 1441151881L >>> 49).toInt // divide a small positive long by 100000000 - write(q, out) + writeMantissa(q, out) write8Digits((q1 - q * 100000000L).toInt, out) } write8Digits((q0 - q1 * 100000000L).toInt, out) @@ -593,6 +603,14 @@ object SafeNumbers { @inline private[json] def write2Digits(x: Int, out: Write): Unit = out.write(digits(x)) + private[this] final val pow10ints: Array[Int] = + Array(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000) + + private[this] final val pow10longs: Array[Long] = + Array(1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 10000000000L, + 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, + 100000000000000000L) + private[this] final val digits: Array[Short] = Array( 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, diff --git a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala index 506be9bcc..a1a3dad4d 100644 --- a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -181,14 +181,19 @@ object SafeNumbers { }) out.write('0') writeMantissa(stripTrailingZeros(dv), out) } else { - exp += 1 - if (exp < len) { - val w = writes.get - writeMantissa(stripTrailingZeros(dv), w) - val cs = w.getChars - out.write(cs, 0, exp) + var pow10i = len - exp - 1 + if (pow10i > 0) { + val pow10 = pow10longs(pow10i) + val q = dv / pow10 + val r = dv - q * pow10 + writeMantissa(q, out) out.write('.') - out.write(cs, exp, w.length) + pow10i -= digitCount(r) + while (pow10i > 0) { + out.write('0') + pow10i -= 1 + } + writeMantissa(stripTrailingZeros(r), out) } else { writeMantissa(dv.toInt, out) out.write('.', '0') @@ -277,14 +282,19 @@ object SafeNumbers { }) out.write('0') writeMantissa(stripTrailingZeros(dv), out) } else { - exp += 1 - if (exp < len) { - val w = writes.get - writeMantissa(stripTrailingZeros(dv), w) - val cs = w.getChars - out.write(cs, 0, exp) + var pow10i = len - exp - 1 + if (pow10i > 0) { + val pow10 = pow10ints(pow10i) + val q = dv / pow10 + val r = dv - q * pow10 + writeMantissa(q, out) out.write('.') - out.write(cs, exp, w.length) + pow10i -= digitCount(r.toLong) + while (pow10i > 0) { + out.write('0') + pow10i -= 1 + } + writeMantissa(stripTrailingZeros(r), out) } else { writeMantissa(dv, out) out.write('.', '0') @@ -382,14 +392,14 @@ object SafeNumbers { } } val m1 = 100000000L - if (q0 < m1) write(q0.toInt, out) + if (q0 < m1) writeMantissa(q0.toInt, out) else { val m2 = 6189700196426901375L val q1 = Math.multiplyHigh(q0, m2) >>> 25 // divide a positive long by 100000000 - if (q1 < m1) write(q1.toInt, out) + if (q1 < m1) writeMantissa(q1.toInt, out) else { val q2 = Math.multiplyHigh(q1, m2) >>> 25 // divide a small positive long by 100000000 - write(q2.toInt, out) + writeMantissa(q2.toInt, out) write8Digits((q1 - q2 * m1).toInt, out) } write8Digits((q0 - q1 * m1).toInt, out) @@ -542,6 +552,14 @@ object SafeNumbers { @inline private[json] def write2Digits(x: Int, out: Write): Unit = out.write(digits(x)) + private[this] final val pow10ints: Array[Int] = + Array(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000) + + private[this] final val pow10longs: Array[Long] = + Array(1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 10000000000L, + 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, + 100000000000000000L) + private[this] final val digits: Array[Short] = Array( 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, From 2646c9ab40183c9ab02a86356c8219b1045ab703 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Tue, 18 Feb 2025 07:39:47 +0100 Subject: [PATCH 170/311] Update magnolia to 1.3.14 (#1321) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 1cb6ce740..804dbe8cd 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Seq( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.13" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.14" ) case _ => Seq( From 1bcfe9b89383c3f5c30d2e591e3973e38d14d9c0 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Tue, 18 Feb 2025 07:41:40 +0100 Subject: [PATCH 171/311] Update scalafmt-core to 3.9.0 (#1322) --- .git-blame-ignore-revs | 3 +++ .scalafmt.conf | 2 +- examples/zio-json-golden/build.sbt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 8da422c19..6923f3c94 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,5 @@ # Scala Steward: Reformat with scalafmt 3.8.6 35c27463b6f38a27b119cc1d82f2e0e49e335789 + +# Scala Steward: Reformat with scalafmt 3.9.0 +47449d54fdda17becd0fce8efd14c894563773c0 diff --git a/.scalafmt.conf b/.scalafmt.conf index ebf4d16ce..1e7307b1e 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.8.6" +version = "3.9.0" runner.dialect = scala213 maxColumn = 120 align.preset = most diff --git a/examples/zio-json-golden/build.sbt b/examples/zio-json-golden/build.sbt index 6eb909e21..d60a52807 100644 --- a/examples/zio-json-golden/build.sbt +++ b/examples/zio-json-golden/build.sbt @@ -1,2 +1,2 @@ -scalaVersion := "2.13.16" +scalaVersion := "2.13.16" libraryDependencies += "dev.zio" %% "zio-json-golden" % "0.7.8" From bbaf2b4d127525fa227be00230113d4dd091a4bf Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 18 Feb 2025 14:01:50 +0100 Subject: [PATCH 172/311] More efficient encoding of `BigInt` and `java.math.BigInteger` values (#1323) --- .../scala/zio/json/internal/SafeNumbers.scala | 130 ++++++++++++++---- .../scala/zio/json/internal/SafeNumbers.scala | 127 +++++++++++++---- .../src/main/scala/zio/json/JsonEncoder.scala | 14 +- 3 files changed, 211 insertions(+), 60 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 9e76042e8..761310ed7 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -78,14 +78,20 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: java.math.BigInteger): String = { + val out = writes.get + write(x, out) + out.buffer.toString + } + def toString(x: Double): String = { - val out = new FastStringWrite(24) + val out = writes.get write(x, out) out.buffer.toString } def toString(x: Float): String = { - val out = new FastStringWrite(16) + val out = writes.get write(x, out) out.buffer.toString } @@ -96,6 +102,59 @@ object SafeNumbers { out.buffer.toString } + def write(x: java.math.BigInteger, out: Write): Unit = writeBigInteger(x, null, out) + + private[this] def writeBigInteger(x: java.math.BigInteger, ss: Array[java.math.BigInteger], out: Write): Unit = { + val bitLen = x.bitLength + if (bitLen < 64) write(x.longValue, out) + else { + val n = calculateTenPow18SquareNumber(bitLen) + val ss1 = + if (ss eq null) getTenPow18Squares(n) + else ss + val qr = x.divideAndRemainder(ss1(n)) + writeBigInteger(qr(0), ss1, out) + writeBigIntegerRemainder(qr(1), n - 1, ss1, out) + } + } + + private[this] def writeBigIntegerRemainder( + x: java.math.BigInteger, + n: Int, + ss: Array[java.math.BigInteger], + out: Write + ): Unit = + if (n < 0) write18Digits(Math.abs(x.longValue), out) + else { + val qr = x.divideAndRemainder(ss(n)) + writeBigIntegerRemainder(qr(0), n - 1, ss, out) + writeBigIntegerRemainder(qr(1), n - 1, ss, out) + } + + private[this] def calculateTenPow18SquareNumber(bitLen: Int): Int = { + val m = Math.max( + (bitLen * 0.016723888647998956).toInt - 1, + 1 + ) // Math.max((x.bitLength * Math.log(2) / Math.log(1e18)).toInt - 1, 1) + 31 - java.lang.Integer.numberOfLeadingZeros(m) + } + + private[this] def getTenPow18Squares(n: Int): Array[java.math.BigInteger] = { + var ss = tenPow18Squares + var i = ss.length + if (n >= i) { + var s = ss(i - 1) + ss = java.util.Arrays.copyOf(ss, n + 1) + while (i <= n) { + s = s.multiply(s) + ss(i) = s + i += 1 + } + tenPow18Squares = ss + } + ss + } + // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java @@ -343,16 +402,6 @@ object SafeNumbers { write(stripTrailingZeros(x), out) } - private[this] val writes = new ThreadLocal[FastStringWrite] { - override def initialValue(): FastStringWrite = new FastStringWrite(24) - - override def get: FastStringWrite = { - val w = super.get - w.reset() - w - } - } - @inline private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { val x = multiplyHigh(g0, cp) + (g1 * cp >>> 1) var y = multiplyHigh(g1, cp) @@ -578,6 +627,14 @@ object SafeNumbers { } } + @inline private[this] def write18Digits(x: Long, out: Write): Unit = { + val q1 = ((x >>> 8) * 2.56e-6).toLong // divide a medium positive long by 100000000 + val q2 = (q1 >>> 8) * 1441151881L >>> 49 // divide a small positive long by 100000000 + out.write(digits(q2.toInt)) + write8Digits((q1 - q2 * 100000000L).toInt, out) + write8Digits((x - q1 * 100000000L).toInt, out) + } + private[this] def write8Digits(x: Int, out: Write): Unit = { val ds = digits val q1 = x / 10000 @@ -603,24 +660,6 @@ object SafeNumbers { @inline private[json] def write2Digits(x: Int, out: Write): Unit = out.write(digits(x)) - private[this] final val pow10ints: Array[Int] = - Array(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000) - - private[this] final val pow10longs: Array[Long] = - Array(1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 10000000000L, - 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, - 100000000000000000L) - - private[this] final val digits: Array[Short] = Array( - 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, - 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, - 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, - 14388, 14644, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 12342, 12598, 12854, 13110, - 13366, 13622, 13878, 14134, 14390, 14646, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, - 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 12345, 12601, 12857, 13113, 13369, 13625, - 13881, 14137, 14393, 14649 - ) - @inline private[this] def digitCount(x: Long): Int = if (x >= 1000000000000000L) { @@ -655,6 +694,16 @@ object SafeNumbers { else 10 } + private[this] final val digits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, + 13873, 14129, 14385, 14641, 12338, 12594, 12850, 13106, 13362, 13618, 13874, 14130, 14386, 14642, 12339, 12595, + 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, + 14388, 14644, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 12342, 12598, 12854, 13110, + 13366, 13622, 13878, 14134, 14390, 14646, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, + 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 12345, 12601, 12857, 13113, 13369, 13625, + 13881, 14137, 14393, 14649 + ) + private[this] final val lowerCaseHexDigits: Array[Short] = Array( 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 24880, 25136, 25392, 25648, 25904, 26160, 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 24881, 25137, 25393, 25649, 25905, 26161, @@ -919,4 +968,25 @@ object SafeNumbers { 8988465674311579538L, 5963149404718312264L, 7190772539449263630L, 8459868338516560134L, 5752618031559410904L, 6767894670813248108L, 9204188850495057447L, 5294608251188331487L ) + + private[this] final val pow10ints: Array[Int] = + Array(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000) + + private[this] final val pow10longs: Array[Long] = + Array(1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 10000000000L, + 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, + 100000000000000000L) + + @volatile private[this] var tenPow18Squares: Array[java.math.BigInteger] = + Array(java.math.BigInteger.valueOf(1000000000000000000L)) + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(64) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w + } + } } diff --git a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala index a1a3dad4d..9dc3b3ea3 100644 --- a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -78,14 +78,20 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: java.math.BigInteger): String = { + val out = writes.get + write(x, out) + out.buffer.toString + } + def toString(x: Double): String = { - val out = new FastStringWrite(24) + val out = writes.get write(x, out) out.buffer.toString } def toString(x: Float): String = { - val out = new FastStringWrite(16) + val out = writes.get write(x, out) out.buffer.toString } @@ -96,6 +102,59 @@ object SafeNumbers { out.buffer.toString } + def write(x: java.math.BigInteger, out: Write): Unit = writeBigInteger(x, null, out) + + private[this] def writeBigInteger(x: java.math.BigInteger, ss: Array[java.math.BigInteger], out: Write): Unit = { + val bitLen = x.bitLength + if (bitLen < 64) write(x.longValue, out) + else { + val n = calculateTenPow18SquareNumber(bitLen) + val ss1 = + if (ss eq null) getTenPow18Squares(n) + else ss + val qr = x.divideAndRemainder(ss1(n)) + writeBigInteger(qr(0), ss1, out) + writeBigIntegerRemainder(qr(1), n - 1, ss1, out) + } + } + + private[this] def writeBigIntegerRemainder( + x: java.math.BigInteger, + n: Int, + ss: Array[java.math.BigInteger], + out: Write + ): Unit = + if (n < 0) write18Digits(Math.abs(x.longValue), out) + else { + val qr = x.divideAndRemainder(ss(n)) + writeBigIntegerRemainder(qr(0), n - 1, ss, out) + writeBigIntegerRemainder(qr(1), n - 1, ss, out) + } + + private[this] def calculateTenPow18SquareNumber(bitLen: Int): Int = { + val m = Math.max( + (bitLen * 71828554L >> 32).toInt - 1, + 1 + ) // Math.max((x.bitLength * Math.log(2) / Math.log(1e18)).toInt - 1, 1) + 31 - java.lang.Integer.numberOfLeadingZeros(m) + } + + private[this] def getTenPow18Squares(n: Int): Array[java.math.BigInteger] = { + var ss = tenPow18Squares + var i = ss.length + if (n >= i) { + var s = ss(i - 1) + ss = java.util.Arrays.copyOf(ss, n + 1) + while (i <= n) { + s = s.multiply(s) + ss(i) = s + i += 1 + } + tenPow18Squares = ss + } + ss + } + // Based on the amazing work of Raffaello Giulietti // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java @@ -334,16 +393,6 @@ object SafeNumbers { write(stripTrailingZeros(x), out) } - private[this] val writes = new ThreadLocal[FastStringWrite] { - override def initialValue(): FastStringWrite = new FastStringWrite(24) - - override def get: FastStringWrite = { - val w = super.get - w.reset() - w - } - } - private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { val x = Math.multiplyHigh(g0, cp) + (g1 * cp >>> 1) Math.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63 @@ -400,9 +449,9 @@ object SafeNumbers { else { val q2 = Math.multiplyHigh(q1, m2) >>> 25 // divide a small positive long by 100000000 writeMantissa(q2.toInt, out) - write8Digits((q1 - q2 * m1).toInt, out) + write8Digits(q1 - q2 * m1, out) } - write8Digits((q0 - q1 * m1).toInt, out) + write8Digits(q0 - q1 * m1, out) } } @@ -411,7 +460,7 @@ object SafeNumbers { else { val q1 = Math.multiplyHigh(q0, 6189700196426901375L) >>> 25 // divide a positive long by 100000000 writeMantissa(q1.toInt, out) - write8Digits((q0 - q1 * 100000000L).toInt, out) + write8Digits(q0 - q1 * 100000000L, out) } private[this] def writeMantissaWithDot(q0: Long, out: Write): Unit = @@ -419,7 +468,7 @@ object SafeNumbers { else { val q1 = Math.multiplyHigh(q0, 6189700196426901375L) >>> 25 // divide a positive long by 100000000 writeMantissaWithDot(q1.toInt, out) - write8Digits((q0 - q1 * 100000000L).toInt, out) + write8Digits(q0 - q1 * 100000000L, out) } def write(a: Int, out: Write): Unit = { @@ -526,7 +575,16 @@ object SafeNumbers { } } - private[this] def write8Digits(x: Int, out: Write): Unit = { + private[this] def write18Digits(x: Long, out: Write): Unit = { + val m1 = 6189700196426901375L + val q1 = Math.multiplyHigh(x, m1) >>> 25 // divide a positive long by 100000000 + val q2 = Math.multiplyHigh(q1, m1) >>> 25 // divide a positive long by 100000000 + out.write(digits(q2.toInt)) + write8Digits(q1 - q2 * 100000000L, out) + write8Digits(x - q1 * 100000000L, out) + } + + private[this] def write8Digits(x: Long, out: Write): Unit = { val ds = digits // Based on James Anhalt's algorithm: https://jk-jeon.github.io/posts/2022/02/jeaiii-algorithm/ val y1 = x * 140737489L val m1 = 0x7fffffffffffL @@ -552,13 +610,9 @@ object SafeNumbers { @inline private[json] def write2Digits(x: Int, out: Write): Unit = out.write(digits(x)) - private[this] final val pow10ints: Array[Int] = - Array(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000) - - private[this] final val pow10longs: Array[Long] = - Array(1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 10000000000L, - 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, - 100000000000000000L) + // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: + // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ + private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt private[this] final val digits: Array[Short] = Array( 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 12337, 12593, 12849, 13105, 13361, 13617, @@ -570,10 +624,6 @@ object SafeNumbers { 13881, 14137, 14393, 14649 ) - // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: - // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ - private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt - private[this] val offsets = Array( 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 4889916394579099648L, 4889916394579099648L, @@ -854,4 +904,25 @@ object SafeNumbers { 8988465674311579538L, 5963149404718312264L, 7190772539449263630L, 8459868338516560134L, 5752618031559410904L, 6767894670813248108L, 9204188850495057447L, 5294608251188331487L ) + + private[this] final val pow10ints: Array[Int] = + Array(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000) + + private[this] final val pow10longs: Array[Long] = + Array(1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 10000000000L, + 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, + 100000000000000000L) + + @volatile private[this] var tenPow18Squares: Array[java.math.BigInteger] = + Array(java.math.BigInteger.valueOf(1000000000000000000L)) + + private[this] val writes = new ThreadLocal[FastStringWrite] { + override def initialValue(): FastStringWrite = new FastStringWrite(64) + + override def get: FastStringWrite = { + val w = super.get + w.reset() + w + } + } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 6b5045a13..d41c38d09 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -232,8 +232,18 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def toJsonAST(a: Long): Either[String, Json] = new Right(Json.Num(a)) } - implicit val bigInteger: JsonEncoder[java.math.BigInteger] = explicit(_.toString, Json.Num.apply) - implicit val scalaBigInt: JsonEncoder[BigInt] = explicit(_.toString, Json.Num.apply) + implicit val bigInteger: JsonEncoder[java.math.BigInteger] = new JsonEncoder[java.math.BigInteger] { + def unsafeEncode(a: java.math.BigInteger, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: java.math.BigInteger): Either[String, Json] = new Right(Json.Num(a)) + } + implicit val scalaBigInt: JsonEncoder[BigInt] = new JsonEncoder[BigInt] { + def unsafeEncode(a: BigInt, indent: Option[Int], out: Write): Unit = + if (a.isValidLong) SafeNumbers.write(a.longValue, out) + else SafeNumbers.write(a.bigInteger, out) + + override def toJsonAST(a: BigInt): Either[String, Json] = new Right(Json.Num(a)) + } implicit val double: JsonEncoder[Double] = new JsonEncoder[Double] { def unsafeEncode(a: Double, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) From b4cedad816f0a02f2e07a3278ad4511291ec314f Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 18 Feb 2025 17:49:18 +0100 Subject: [PATCH 173/311] More efficient encoding of `BigDecimal` and `java.math.BigDecimal` values (#1324) --- .../scala/zio/json/internal/SafeNumbers.scala | 126 +++++++++++++++++- .../scala/zio/json/internal/SafeNumbers.scala | 123 ++++++++++++++++- .../src/main/scala/zio/json/JsonEncoder.scala | 16 ++- .../src/test/scala/zio/json/EncoderSpec.scala | 32 +++-- .../shared/src/test/scala/zio/json/Gens.scala | 11 +- .../test/scala/zio/json/RoundTripSpec.scala | 3 + 6 files changed, 287 insertions(+), 24 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 761310ed7..61fba7803 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -78,6 +78,12 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: java.math.BigDecimal): String = { + val out = writes.get + write(x, out) + out.buffer.toString + } + def toString(x: java.math.BigInteger): String = { val out = writes.get write(x, out) @@ -102,6 +108,121 @@ object SafeNumbers { out.buffer.toString } + def write(x: java.math.BigDecimal, out: Write): Unit = { + var exp = writeBigDecimal(x.unscaledValue, x.scale, 0, null, out) + if (exp != 0) { + var sc = '+' + if (exp < 0) { + sc = '-' + exp = -exp + } + out.write('E', sc) + writeMantissa(exp, out) + } + } + + private[this] def writeBigDecimal( + x: java.math.BigInteger, + scale: Int, + blockScale: Int, + ss: Array[java.math.BigInteger], + out: Write + ): Int = { + val bitLen = x.bitLength + if (bitLen < 64) { + val v = x.longValue + val pv = Math.abs(v) + val digits = + if (pv >= 100000000000000000L) { + if (pv >= 1000000000000000000L) 19 + else 18 + } else digitCount(pv) + val dotOff = scale - blockScale + val exp = (digits - 1) - dotOff + if (scale >= 0 && exp >= -6) { + if (exp < 0) { + out.write('0', '.') + var zeros = -exp - 1 + while (zeros > 0) { + out.write('0') + zeros -= 1 + } + write(v, out) + } else if (dotOff > 0) writeLongWithDot(v, dotOff, out) + else write(v, out) + 0 + } else { + if (digits > 1) writeLongWithDot(v, digits - 1, out) + else { + write(v, out) + if (blockScale > 0) out.write('.') + } + exp + } + } else { + val n = calculateTenPow18SquareNumber(bitLen) + val ss1 = + if (ss eq null) getTenPow18Squares(n) + else ss + val qr = x.divideAndRemainder(ss1(n)) + val exp = writeBigDecimal(qr(0), scale, (18 << n) + blockScale, ss1, out) + writeBigDecimalRemainder(qr(1), scale, blockScale, n - 1, ss1, out) + exp + } + } + + @inline private[this] def writeLongWithDot(v: Long, dotOff: Int, out: Write): Unit = { + val pow10 = pow10longs(dotOff) + val q = v / pow10 + val r = Math.abs(v - q * pow10) + write(q, out) + out.write('.') + var zeros = dotOff - digitCount(r) + while (zeros > 0) { + out.write('0') + zeros -= 1 + } + write(r, out) + } + + private[this] def writeBigDecimalRemainder( + x: java.math.BigInteger, + scale: Int, + blockScale: Int, + n: Int, + ss: Array[java.math.BigInteger], + out: Write + ): Unit = + if (n < 0) { + val v = Math.abs(x.longValue) + var dotOff = scale - blockScale + if (dotOff > 0 && dotOff < 18) { + val pow10 = pow10longs(dotOff) + val q = v / pow10 + val r = v - q * pow10 + var zeros = 18 - dotOff - digitCount(q) + while (zeros > 0) { + out.write('0') + zeros -= 1 + } + writeMantissa(q, out) + out.write('.') + dotOff -= digitCount(r) + while (dotOff > 0) { + out.write('0') + dotOff -= 1 + } + writeMantissa(r, out) + } else { + if (dotOff == 18) out.write('.') + write18Digits(v, out) + } + } else { + val qr = x.divideAndRemainder(ss(n)) + writeBigDecimalRemainder(qr(0), scale, (18 << n) + blockScale, n - 1, ss, out) + writeBigDecimalRemainder(qr(1), scale, blockScale, n - 1, ss, out) + } + def write(x: java.math.BigInteger, out: Write): Unit = writeBigInteger(x, null, out) private[this] def writeBigInteger(x: java.math.BigInteger, ss: Array[java.math.BigInteger], out: Write): Unit = { @@ -660,8 +781,7 @@ object SafeNumbers { @inline private[json] def write2Digits(x: Int, out: Write): Unit = out.write(digits(x)) - @inline - private[this] def digitCount(x: Long): Int = + @inline private[this] def digitCount(x: Long): Int = if (x >= 1000000000000000L) { if (x >= 10000000000000000L) 17 else 16 @@ -975,7 +1095,7 @@ object SafeNumbers { private[this] final val pow10longs: Array[Long] = Array(1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 10000000000L, 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, - 100000000000000000L) + 100000000000000000L, 1000000000000000000L) @volatile private[this] var tenPow18Squares: Array[java.math.BigInteger] = Array(java.math.BigInteger.valueOf(1000000000000000000L)) diff --git a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala index 9dc3b3ea3..2d05488c1 100644 --- a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -78,6 +78,12 @@ object SafeNumbers { try Some(UnsafeNumbers.bigDecimal(num, max_bits)) catch { case _: UnexpectedEnd | UnsafeNumber => None } + def toString(x: java.math.BigDecimal): String = { + val out = writes.get + write(x, out) + out.buffer.toString + } + def toString(x: java.math.BigInteger): String = { val out = writes.get write(x, out) @@ -102,6 +108,121 @@ object SafeNumbers { out.buffer.toString } + def write(x: java.math.BigDecimal, out: Write): Unit = { + var exp = writeBigDecimal(x.unscaledValue, x.scale, 0, null, out) + if (exp != 0) { + var sc = '+' + if (exp < 0) { + sc = '-' + exp = -exp + } + out.write('E', sc) + writeMantissa(exp, out) + } + } + + private[this] def writeBigDecimal( + x: java.math.BigInteger, + scale: Int, + blockScale: Int, + ss: Array[java.math.BigInteger], + out: Write + ): Int = { + val bitLen = x.bitLength + if (bitLen < 64) { + val v = x.longValue + val pv = Math.abs(v) + val digits = + if (pv >= 100000000000000000L) { + if (pv >= 1000000000000000000L) 19 + else 18 + } else digitCount(pv) + val dotOff = scale - blockScale + val exp = (digits - 1) - dotOff + if (scale >= 0 && exp >= -6) { + if (exp < 0) { + out.write('0', '.') + var zeros = -exp - 1 + while (zeros > 0) { + out.write('0') + zeros -= 1 + } + write(v, out) + } else if (dotOff > 0) writeLongWithDot(v, dotOff, out) + else write(v, out) + 0 + } else { + if (digits > 1) writeLongWithDot(v, digits - 1, out) + else { + write(v, out) + if (blockScale > 0) out.write('.') + } + exp + } + } else { + val n = calculateTenPow18SquareNumber(bitLen) + val ss1 = + if (ss eq null) getTenPow18Squares(n) + else ss + val qr = x.divideAndRemainder(ss1(n)) + val exp = writeBigDecimal(qr(0), scale, (18 << n) + blockScale, ss1, out) + writeBigDecimalRemainder(qr(1), scale, blockScale, n - 1, ss1, out) + exp + } + } + + private[this] def writeLongWithDot(v: Long, dotOff: Int, out: Write): Unit = { + val pow10 = pow10longs(dotOff) + val q = v / pow10 + val r = Math.abs(v - q * pow10) + write(q, out) + out.write('.') + var zeros = dotOff - digitCount(r) + while (zeros > 0) { + out.write('0') + zeros -= 1 + } + write(r, out) + } + + private[this] def writeBigDecimalRemainder( + x: java.math.BigInteger, + scale: Int, + blockScale: Int, + n: Int, + ss: Array[java.math.BigInteger], + out: Write + ): Unit = + if (n < 0) { + val v = Math.abs(x.longValue) + var dotOff = scale - blockScale + if (dotOff > 0 && dotOff < 18) { + val pow10 = pow10longs(dotOff) + val q = v / pow10 + val r = v - q * pow10 + var zeros = 18 - dotOff - digitCount(q) + while (zeros > 0) { + out.write('0') + zeros -= 1 + } + writeMantissa(q, out) + out.write('.') + dotOff -= digitCount(r) + while (dotOff > 0) { + out.write('0') + dotOff -= 1 + } + writeMantissa(r, out) + } else { + if (dotOff == 18) out.write('.') + write18Digits(v, out) + } + } else { + val qr = x.divideAndRemainder(ss(n)) + writeBigDecimalRemainder(qr(0), scale, (18 << n) + blockScale, n - 1, ss, out) + writeBigDecimalRemainder(qr(1), scale, blockScale, n - 1, ss, out) + } + def write(x: java.math.BigInteger, out: Write): Unit = writeBigInteger(x, null, out) private[this] def writeBigInteger(x: java.math.BigInteger, ss: Array[java.math.BigInteger], out: Write): Unit = { @@ -911,7 +1032,7 @@ object SafeNumbers { private[this] final val pow10longs: Array[Long] = Array(1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 10000000000L, 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, - 100000000000000000L) + 100000000000000000L, 1000000000000000000L) @volatile private[this] var tenPow18Squares: Array[java.math.BigInteger] = Array(java.math.BigInteger.valueOf(1000000000000000000L)) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index d41c38d09..273dd9f5c 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -215,12 +215,12 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with implicit val byte: JsonEncoder[Byte] = new JsonEncoder[Byte] { def unsafeEncode(a: Byte, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a.toInt, out) - override def toJsonAST(a: Byte): Either[String, Json] = new Right(Json.Num(a)) + override def toJsonAST(a: Byte): Either[String, Json] = new Right(Json.Num(a.toInt)) } implicit val short: JsonEncoder[Short] = new JsonEncoder[Short] { def unsafeEncode(a: Short, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a.toInt, out) - override def toJsonAST(a: Short): Either[String, Json] = new Right(Json.Num(a)) + override def toJsonAST(a: Short): Either[String, Json] = new Right(Json.Num(a.toInt)) } implicit val int: JsonEncoder[Int] = new JsonEncoder[Int] { def unsafeEncode(a: Int, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) @@ -254,8 +254,16 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def toJsonAST(a: Float): Either[String, Json] = new Right(Json.Num(a)) } - implicit val bigDecimal: JsonEncoder[java.math.BigDecimal] = explicit(_.toString, n => new Json.Num(n)) - implicit val scalaBigDecimal: JsonEncoder[BigDecimal] = explicit(_.toString, Json.Num.apply) + implicit val bigDecimal: JsonEncoder[java.math.BigDecimal] = new JsonEncoder[java.math.BigDecimal] { + def unsafeEncode(a: java.math.BigDecimal, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a, out) + + override def toJsonAST(a: java.math.BigDecimal): Either[String, Json] = new Right(new Json.Num(a)) + } + implicit val scalaBigDecimal: JsonEncoder[BigDecimal] = new JsonEncoder[BigDecimal] { + def unsafeEncode(a: BigDecimal, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a.bigDecimal, out) + + override def toJsonAST(a: BigDecimal): Either[String, Json] = new Right(new Json.Num(a.bigDecimal)) + } implicit def option[A](implicit A: JsonEncoder[A]): JsonEncoder[Option[A]] = new JsonEncoder[Option[A]] { def unsafeEncode(oa: Option[A], indent: Option[Int], out: Write): Unit = diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index 278512cd2..d498e4989 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -245,20 +245,28 @@ object EncoderSpec extends ZIOSpecDefault { assert((-6939.0464d).toJson)( equalTo("-6939.0464") ) // See the issue: https://github.com/zio/zio-json/pull/375 - }, - test("other numerics") { - val exampleBigIntStr = "170141183460469231731687303715884105728" - val exampleBigDecimalStr = "170141183460469231731687303715884105728.4433" - assert((1: Byte).toJson)(equalTo("1")) && - assert((1: Short).toJson)(equalTo("1")) && - assert((1: Int).toJson)(equalTo("1")) && - assert(1L.toJson)(equalTo("1")) && - assert(new java.math.BigInteger("1").toJson)(equalTo("1")) && - assert(new java.math.BigInteger(exampleBigIntStr).toJson)(equalTo(exampleBigIntStr)) && - assert(BigInt(exampleBigIntStr).toJson)(equalTo(exampleBigIntStr)) && - assert(BigDecimal(exampleBigDecimalStr).toJson)(equalTo(exampleBigDecimalStr)) } ), + test("BigInt") { + assert(BigInt("-1").toJson)(equalTo("-1")) && + assert(BigInt("-316873037158841").toJson)(equalTo("-316873037158841")) && + assert(BigInt("1701411834604692317316873037158841").toJson)(equalTo("1701411834604692317316873037158841")) + }, + test("BigDecimal") { + assert(BigDecimal("-1.0").toJson)(equalTo("-1.0")) && + assert(BigDecimal("1.0E+5").toJson)(equalTo("1.0E+5")) && + assert(BigDecimal("0.000100").toJson)(equalTo("0.000100")) && + assert(BigDecimal("0.000001").toJson)(equalTo("0.000001")) && + assert(BigDecimal("100000.00").toJson)(equalTo("100000.00")) && + assert(BigDecimal("1E-2147483647").toJson)(equalTo("1E-2147483647")) && + assert(BigDecimal("1E+2147483647").toJson)(equalTo("1E+2147483647")) && + assert(BigDecimal("-234316873037.008841").toJson)(equalTo("-234316873037.008841")) && + assert(BigDecimal("141183460469231731687303715.8841").toJson)(equalTo("141183460469231731687303715.8841")) && + assert(BigDecimal("1.7014118346046923173168730E+119").toJson)(equalTo("1.7014118346046923173168730E+119")) && + assert( + BigDecimal("-9.999999999999874791608720182523363282786709588281885514820801359042815031E-4571018").toJson + )(equalTo("-9.999999999999874791608720182523363282786709588281885514820801359042815031E-4571018")) + }, test("options") { assert((None: Option[Int]).toJson)(equalTo("null")) && assert((Some(1): Option[Int]).toJson)(equalTo("1")) diff --git a/zio-json/shared/src/test/scala/zio/json/Gens.scala b/zio-json/shared/src/test/scala/zio/json/Gens.scala index 417956559..1e72df0a3 100644 --- a/zio-json/shared/src/test/scala/zio/json/Gens.scala +++ b/zio-json/shared/src/test/scala/zio/json/Gens.scala @@ -14,10 +14,13 @@ object Gens { .filter(_.bitLength < 256) val genBigDecimal = - Gen - .bigDecimal((BigDecimal(2).pow(256) - 1) * -1, BigDecimal(2).pow(256) - 1) - .map(_.bigDecimal) - .filter(_.unscaledValue.bitLength < 256) + for { + unscaled <- Gen + .bigInt((BigInt(2).pow(256) - 1) * -1, BigInt(2).pow(256) - 1) + .map(_.bigInteger) + .filter(_.bitLength < 256) + scale <- Gen.oneOf(Gen.int(-20, 20), Gen.int(-1000000000, 1000000000)) + } yield new java.math.BigDecimal(unscaled, scale) val genUsAsciiString = Gen.string(Gen.oneOf(Gen.char('!', '~'))) diff --git a/zio-json/shared/src/test/scala/zio/json/RoundTripSpec.scala b/zio-json/shared/src/test/scala/zio/json/RoundTripSpec.scala index dcca2473e..3e62faf98 100644 --- a/zio-json/shared/src/test/scala/zio/json/RoundTripSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/RoundTripSpec.scala @@ -30,6 +30,9 @@ object RoundTripSpec extends ZIOSpecDefault { test("bigInts") { check(genBigInteger)(assertRoundtrips[java.math.BigInteger]) } @@ jvm(samples(10000)), + test("bigDecimals") { + check(genBigDecimal)(assertRoundtrips[java.math.BigDecimal]) + } @@ jvm(samples(10000)), test("floats") { // NaN / Infinity is tested manually, because of == semantics check(Gen.float.filter(java.lang.Float.isFinite))(assertRoundtrips[Float]) From 853af501f91ee6f92ec20ede342256234bfd1477 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 18 Feb 2025 21:32:18 +0100 Subject: [PATCH 174/311] Fix encoding of negative `BigDecimal` and `java.math.BigDecimal` values (#1325) --- .../js/src/main/scala/zio/json/internal/SafeNumbers.scala | 5 +++-- .../src/main/scala/zio/json/internal/SafeNumbers.scala | 5 +++-- zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 61fba7803..00f61756b 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -141,13 +141,14 @@ object SafeNumbers { val exp = (digits - 1) - dotOff if (scale >= 0 && exp >= -6) { if (exp < 0) { - out.write('0', '.') + if (v >= 0) out.write('0', '.') + else out.write('-', '0', '.') var zeros = -exp - 1 while (zeros > 0) { out.write('0') zeros -= 1 } - write(v, out) + write(pv, out) } else if (dotOff > 0) writeLongWithDot(v, dotOff, out) else write(v, out) 0 diff --git a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala index 2d05488c1..f06428d24 100644 --- a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -141,13 +141,14 @@ object SafeNumbers { val exp = (digits - 1) - dotOff if (scale >= 0 && exp >= -6) { if (exp < 0) { - out.write('0', '.') + if (v >= 0) out.write('0', '.') + else out.write('-', '0', '.') var zeros = -exp - 1 while (zeros > 0) { out.write('0') zeros -= 1 } - write(v, out) + write(pv, out) } else if (dotOff > 0) writeLongWithDot(v, dotOff, out) else write(v, out) 0 diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index d498e4989..74b9b4d2e 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -256,7 +256,7 @@ object EncoderSpec extends ZIOSpecDefault { assert(BigDecimal("-1.0").toJson)(equalTo("-1.0")) && assert(BigDecimal("1.0E+5").toJson)(equalTo("1.0E+5")) && assert(BigDecimal("0.000100").toJson)(equalTo("0.000100")) && - assert(BigDecimal("0.000001").toJson)(equalTo("0.000001")) && + assert(BigDecimal("-0.000001").toJson)(equalTo("-0.000001")) && assert(BigDecimal("100000.00").toJson)(equalTo("100000.00")) && assert(BigDecimal("1E-2147483647").toJson)(equalTo("1E-2147483647")) && assert(BigDecimal("1E+2147483647").toJson)(equalTo("1E+2147483647")) && From 2afada802e8b31c7214c02762dc8e0ffef0081ef Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 19 Feb 2025 07:45:52 +0100 Subject: [PATCH 175/311] Fix encoding of `BigDecimal` and `java.math.BigDecimal` values for Scala.js (#1326) --- zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala | 2 +- zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 00f61756b..0a6062549 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -178,7 +178,7 @@ object SafeNumbers { val r = Math.abs(v - q * pow10) write(q, out) out.write('.') - var zeros = dotOff - digitCount(r) + var zeros = dotOff - (if (v >= 100000000000000000L) 18 else digitCount(r)) while (zeros > 0) { out.write('0') zeros -= 1 diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index 74b9b4d2e..7f89836bd 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -261,6 +261,7 @@ object EncoderSpec extends ZIOSpecDefault { assert(BigDecimal("1E-2147483647").toJson)(equalTo("1E-2147483647")) && assert(BigDecimal("1E+2147483647").toJson)(equalTo("1E+2147483647")) && assert(BigDecimal("-234316873037.008841").toJson)(equalTo("-234316873037.008841")) && + assert(BigDecimal("2.999999999999999999E-17").toJson)(equalTo("2.999999999999999999E-17")) && assert(BigDecimal("141183460469231731687303715.8841").toJson)(equalTo("141183460469231731687303715.8841")) && assert(BigDecimal("1.7014118346046923173168730E+119").toJson)(equalTo("1.7014118346046923173168730E+119")) && assert( From b5e3408bd6df29f4dd095607a3a276bb621852f7 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 19 Feb 2025 10:22:21 +0100 Subject: [PATCH 176/311] Fix encoding of `BigDecimal` and `java.math.BigDecimal` values for Scala.js (2nd attempt) (#1327) --- .../scala/zio/json/internal/SafeNumbers.scala | 17 ++++++++--------- .../src/test/scala/zio/json/EncoderSpec.scala | 1 + 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 0a6062549..629ce4498 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -130,13 +130,9 @@ object SafeNumbers { ): Int = { val bitLen = x.bitLength if (bitLen < 64) { - val v = x.longValue - val pv = Math.abs(v) - val digits = - if (pv >= 100000000000000000L) { - if (pv >= 1000000000000000000L) 19 - else 18 - } else digitCount(pv) + val v = x.longValue + val pv = Math.abs(v) + val digits = digitCount(pv) val dotOff = scale - blockScale val exp = (digits - 1) - dotOff if (scale >= 0 && exp >= -6) { @@ -178,7 +174,7 @@ object SafeNumbers { val r = Math.abs(v - q * pow10) write(q, out) out.write('.') - var zeros = dotOff - (if (v >= 100000000000000000L) 18 else digitCount(r)) + var zeros = dotOff - digitCount(r) while (zeros > 0) { out.write('0') zeros -= 1 @@ -783,7 +779,10 @@ object SafeNumbers { out.write(digits(x)) @inline private[this] def digitCount(x: Long): Int = - if (x >= 1000000000000000L) { + if (x >= 100000000000000000L) { + if (x >= 1000000000000000000L) 19 + else 18 + } else if (x >= 1000000000000000L) { if (x >= 10000000000000000L) 17 else 16 } else if (x >= 10000000000000L) { diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index 7f89836bd..c210f703c 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -262,6 +262,7 @@ object EncoderSpec extends ZIOSpecDefault { assert(BigDecimal("1E+2147483647").toJson)(equalTo("1E+2147483647")) && assert(BigDecimal("-234316873037.008841").toJson)(equalTo("-234316873037.008841")) && assert(BigDecimal("2.999999999999999999E-17").toJson)(equalTo("2.999999999999999999E-17")) && + assert(BigDecimal("-7.812738666512280685E-15").toJson)(equalTo("-7.812738666512280685E-15")) && assert(BigDecimal("141183460469231731687303715.8841").toJson)(equalTo("141183460469231731687303715.8841")) && assert(BigDecimal("1.7014118346046923173168730E+119").toJson)(equalTo("1.7014118346046923173168730E+119")) && assert( From 4063bece4de6ad394b721aa37683b90c1c103b38 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 20 Feb 2025 08:47:49 +0100 Subject: [PATCH 177/311] More efficient rendering of errors (#1328) --- .../src/main/scala/zio/json/JsonError.scala | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonError.scala b/zio-json/shared/src/main/scala/zio/json/JsonError.scala index f82ded949..3ee4bb9ef 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonError.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonError.scala @@ -22,14 +22,17 @@ package zio.json sealed abstract class JsonError object JsonError { - def render(trace: List[JsonError]): String = - trace.reverse.map { - case Message(txt) => s"($txt)" - case ArrayAccess(i) => s"[$i]" - case ObjectAccess(field) => s".$field" - case SumType(cons) => s"{$cons}" - }.mkString + trace + .foldRight(new java.lang.StringBuilder) { (err, sb) => + err match { + case o: ObjectAccess => sb.append('.').append(o.field) + case a: ArrayAccess => sb.append('[').append(a.i).append(']') + case s: SumType => sb.append('{').append(s.cons).append('}') + case m: Message => sb.append('(').append(m.txt).append(')') + } + } + .toString final case class Message(txt: String) extends JsonError @@ -38,5 +41,4 @@ object JsonError { final case class ObjectAccess(field: String) extends JsonError final case class SumType(cons: String) extends JsonError - } From 50bd17888267ec388c52d5167fb90356a6fd7db8 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 20 Feb 2025 17:09:34 +0100 Subject: [PATCH 178/311] More efficient default encoding to strings (#1329) --- build.sbt | 5 ++- .../zio/json/internal/FastStringWrite.scala | 6 +-- .../scala/zio/json/internal/SafeNumbers.scala | 10 ++--- .../zio/json/internal/FastStringWrite.scala | 6 +-- .../scala/zio/json/internal/SafeNumbers.scala | 10 ++--- .../src/main/scala/zio/json/JsonEncoder.scala | 39 +++++++++++++++++-- .../scala/zio/json/javatime/serializers.scala | 26 ++++++------- 7 files changed, 65 insertions(+), 37 deletions(-) diff --git a/build.sbt b/build.sbt index 804dbe8cd..395ed9d54 100644 --- a/build.sbt +++ b/build.sbt @@ -227,8 +227,9 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) exclude[Problem]("zio.json.package.*") ), libraryDependencies ++= Seq( - "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, - "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % "test" + ("org.scala-js" %%% "scalajs-weakreferences" % "1.0.0").cross(CrossVersion.for3Use2_13), + "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, + "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % "test" ) ) .nativeSettings(nativeSettings) diff --git a/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala index c9dc72782..811ca9405 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/FastStringWrite.scala @@ -6,10 +6,6 @@ final class FastStringWrite(initial: Int) extends Write { @inline def reset(): Unit = chars = "" - @inline private[internal] def length: Int = chars.length - - @inline private[internal] def getChars: Array[Char] = chars.toCharArray - @inline def write(s: String): Unit = chars += s @inline def write(c: Char): Unit = chars += c @@ -81,4 +77,6 @@ final class FastStringWrite(initial: Int) extends Write { } @inline def buffer: CharSequence = chars + + @inline override def toString: String = chars } diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 629ce4498..427b635cc 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -81,31 +81,31 @@ object SafeNumbers { def toString(x: java.math.BigDecimal): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def toString(x: java.math.BigInteger): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def toString(x: Double): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def toString(x: Float): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def toString(x: UUID): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: java.math.BigDecimal, out: Write): Unit = { diff --git a/zio-json/jvm-native/src/main/scala/zio/json/internal/FastStringWrite.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/FastStringWrite.scala index 107d894d6..ef8cc9b31 100644 --- a/zio-json/jvm-native/src/main/scala/zio/json/internal/FastStringWrite.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/FastStringWrite.scala @@ -10,10 +10,6 @@ final class FastStringWrite(initial: Int) extends Write { @inline def reset(): Unit = count = 0 - @inline private[internal] def length: Int = count - - @inline private[internal] def getChars: Array[Char] = chars - def write(s: String): Unit = { val l = s.length var cs = chars @@ -168,4 +164,6 @@ final class FastStringWrite(initial: Int) extends Write { } def buffer: CharSequence = CharBuffer.wrap(chars, 0, count) + + override def toString: String = new String(chars, 0, count) } diff --git a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala index f06428d24..c30209e44 100644 --- a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -81,31 +81,31 @@ object SafeNumbers { def toString(x: java.math.BigDecimal): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def toString(x: java.math.BigInteger): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def toString(x: Double): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def toString(x: Float): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def toString(x: UUID): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: java.math.BigDecimal, out: Write): Unit = { diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 273dd9f5c..82fbc5993 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -19,7 +19,6 @@ import zio.json.ast.Json import zio.json.internal.{ FastStringWrite, SafeNumbers, Write } import zio.json.javatime.serializers import zio.{ Chunk, NonEmptyChunk } - import java.util.UUID import scala.annotation._ import scala.collection.{ immutable, mutable } @@ -67,9 +66,12 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { * Encodes the specified value into a JSON string, with the specified indentation level. */ final def encodeJson(a: A, indent: Option[Int] = None): CharSequence = { - val writer = new FastStringWrite(64) - unsafeEncode(a, indent, writer) - writer.buffer + val writePool = JsonEncoder.writePools.get + try { + val write = writePool.acquire() + unsafeEncode(a, indent, write) + write.toString + } finally writePool.release() } /** @@ -111,6 +113,35 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { } object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with JsonEncoderVersionSpecific { + private class FastStringWritePool { + private[this] var weakRef: java.lang.ref.WeakReference[Array[FastStringWrite]] = + new java.lang.ref.WeakReference(Array(new FastStringWrite(64))) + private[this] var level: Int = 0 + + def acquire(): FastStringWrite = { + var writes = weakRef.get + if (writes eq null) { // the reference was collected by GC + level = 0 + writes = new Array(0) + } + if (level == writes.length) { // exceding the deepest level of recusion + writes = java.util.Arrays.copyOf(writes, level + 1) + writes(level) = new FastStringWrite(64) + weakRef = new java.lang.ref.WeakReference(writes) + } + val write = writes(level) + level += 1 // increase the level of recusrion + write.reset() + write + } + + def release(): Unit = level -= 1 // decrease the level of recusrion + } + + private val writePools = new ThreadLocal[FastStringWritePool] { + override def initialValue(): FastStringWritePool = new FastStringWritePool + } + @inline def apply[A](implicit a: JsonEncoder[A]): JsonEncoder[A] = a implicit val string: JsonEncoder[String] = new JsonEncoder[String] { diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala index 3ac9b581b..89671f1cd 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala @@ -23,7 +23,7 @@ private[json] object serializers { def toString(x: Duration): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: Duration, out: Write): Unit = { @@ -61,7 +61,7 @@ private[json] object serializers { def toString(x: Instant): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: Instant, out: Write): Unit = { @@ -124,7 +124,7 @@ private[json] object serializers { def toString(x: LocalDate): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: LocalDate, out: Write): Unit = { @@ -138,7 +138,7 @@ private[json] object serializers { def toString(x: LocalDateTime): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: LocalDateTime, out: Write): Unit = { @@ -150,7 +150,7 @@ private[json] object serializers { def toString(x: LocalTime): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: LocalTime, out: Write): Unit = { @@ -166,7 +166,7 @@ private[json] object serializers { def toString(x: MonthDay): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: MonthDay, out: Write): Unit = { @@ -179,7 +179,7 @@ private[json] object serializers { def toString(x: OffsetDateTime): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: OffsetDateTime, out: Write): Unit = { @@ -192,7 +192,7 @@ private[json] object serializers { def toString(x: OffsetTime): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: OffsetTime, out: Write): Unit = { @@ -203,7 +203,7 @@ private[json] object serializers { def toString(x: Period): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: Period, out: Write): Unit = { @@ -231,7 +231,7 @@ private[json] object serializers { def toString(x: Year): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } @inline def write(x: Year, out: Write): Unit = writeYear(x.getValue, out) @@ -239,7 +239,7 @@ private[json] object serializers { def toString(x: YearMonth): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: YearMonth, out: Write): Unit = { @@ -251,7 +251,7 @@ private[json] object serializers { def toString(x: ZonedDateTime): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: ZonedDateTime, out: Write): Unit = { @@ -274,7 +274,7 @@ private[json] object serializers { def toString(x: ZoneOffset): String = { val out = writes.get write(x, out) - out.buffer.toString + out.toString } def write(x: ZoneOffset, out: Write): Unit = { From 46ccfab13a9032d432e0104331bd3c361a22d196 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 21 Feb 2025 13:45:16 +0100 Subject: [PATCH 179/311] More efficient encoding of remaining standard types (#1330) --- .../scala/zio/json/internal/SafeNumbers.scala | 5 ++ .../scala/zio/json/internal/SafeNumbers.scala | 5 ++ .../src/main/scala/zio/json/JsonEncoder.scala | 73 ++++++++++++++----- .../scala/zio/json/internal/readers.scala | 18 +++-- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index 427b635cc..b6f0403cc 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -510,6 +510,11 @@ object SafeNumbers { out.write(ds(lsb2 >>> 24), ds(lsb2 >> 16 & 0xff), ds(lsb2 >> 8 & 0xff), ds(lsb2 & 0xff)) } + private[json] def writeHex(c: Char, out: Write): Unit = { + val ds = lowerCaseHexDigits + out.write(ds(c >> 8 & 0xff), ds(c & 0xff)) + } + private[json] def writeNano(x: Int, out: Write): Unit = { out.write('.') var coeff = 100000000 diff --git a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala index c30209e44..40f08be75 100644 --- a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -505,6 +505,11 @@ object SafeNumbers { out.write(ds(lsb2 >>> 24), ds(lsb2 >> 16 & 0xff), ds(lsb2 >> 8 & 0xff), ds(lsb2 & 0xff)) } + private[json] def writeHex(c: Char, out: Write): Unit = { + val ds = lowerCaseHexDigits + out.write(ds(c >> 8 & 0xff), ds(c & 0xff)) + } + private[json] def writeNano(x: Int, out: Write): Unit = { out.write('.') var coeff = 100000000 diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 82fbc5993..1cad8acc8 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -120,6 +120,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with def acquire(): FastStringWrite = { var writes = weakRef.get + var level = this.level if (writes eq null) { // the reference was collected by GC level = 0 writes = new Array(0) @@ -130,7 +131,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with weakRef = new java.lang.ref.WeakReference(writes) } val write = writes(level) - level += 1 // increase the level of recusrion + this.level = level + 1 // increase the level of recusrion write.reset() write } @@ -176,8 +177,11 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with case '\r' => out.write('\\', 'r') case '\t' => out.write('\\', 't') case c => - if (c < ' ') out.write("\\u%04x".format(c.toInt)) - else out.write(c) + if (c >= ' ') out.write(c) + else { + out.write('\\', 'u') + SafeNumbers.writeHex(c, out) + } } i += 1 } @@ -186,32 +190,35 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with } implicit val char: JsonEncoder[Char] = new JsonEncoder[Char] { - override def unsafeEncode(a: Char, indent: Option[Int], out: Write): Unit = { - out.write('"') + override def unsafeEncode(a: Char, indent: Option[Int], out: Write): Unit = (a: @switch) match { - case '"' => out.write('\\', '"') - case '\\' => out.write('\\', '\\') - case '\b' => out.write('\\', 'b') - case '\f' => out.write('\\', 'f') - case '\n' => out.write('\\', 'n') - case '\r' => out.write('\\', 'r') - case '\t' => out.write('\\', 't') + case '"' => out.write('"', '\\', '"', '"') + case '\\' => out.write('"', '\\', '\\', '"') + case '\b' => out.write('"', '\\', 'b', '"') + case '\f' => out.write('"', '\\', 'f', '"') + case '\n' => out.write('"', '\\', 'n', '"') + case '\r' => out.write('"', '\\', 'r', '"') + case '\t' => out.write('"', '\\', 't', '"') case c => - if (c < ' ') out.write("\\u%04x".format(c.toInt)) - else out.write(c) + if (c >= ' ') out.write('"', c, '"') + else { + out.write('"', '\\', 'u') + SafeNumbers.writeHex(c, out) + out.write('"') + } } - out.write('"') - } override final def toJsonAST(a: Char): Either[String, Json] = new Right(new Json.Str(a.toString)) } + // FIXME: remove in the next major version private[json] def explicit[A](f: A => String, g: A => Json): JsonEncoder[A] = new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = out.write(f(a)) override final def toJsonAST(a: A): Either[String, Json] = new Right(g(a)) } + // FIXME: remove in the next major version private[json] def stringify[A](f: A => String): JsonEncoder[A] = new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { out.write('"') @@ -222,6 +229,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override final def toJsonAST(a: A): Either[String, Json] = new Right(new Json.Str(f(a))) } + // FIXME: add tests def suspend[A](encoder0: => JsonEncoder[A]): JsonEncoder[A] = new JsonEncoder[A] { lazy val encoder = encoder0 @@ -633,7 +641,16 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { import java.time._ - implicit val dayOfWeek: JsonEncoder[DayOfWeek] = stringify(_.toString) + implicit val dayOfWeek: JsonEncoder[DayOfWeek] = new JsonEncoder[DayOfWeek] { + def unsafeEncode(a: DayOfWeek, indent: Option[Int], out: Write): Unit = { + out.write('"') + out.write(a.toString) + out.write('"') + } + + override final def toJsonAST(a: DayOfWeek): Either[String, Json] = + new Right(new Json.Str(a.toString)) + } implicit val duration: JsonEncoder[Duration] = new JsonEncoder[Duration] { def unsafeEncode(a: Duration, indent: Option[Int], out: Write): Unit = { @@ -690,7 +707,16 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { new Right(new Json.Str(serializers.toString(a))) } - implicit val month: JsonEncoder[Month] = stringify(_.toString) + implicit val month: JsonEncoder[Month] = new JsonEncoder[Month] { + def unsafeEncode(a: Month, indent: Option[Int], out: Write): Unit = { + out.write('"') + out.write(a.toString) + out.write('"') + } + + override final def toJsonAST(a: Month): Either[String, Json] = + new Right(new Json.Str(a.toString)) + } implicit val monthDay: JsonEncoder[MonthDay] = new JsonEncoder[MonthDay] { def unsafeEncode(a: MonthDay, indent: Option[Int], out: Write): Unit = { @@ -802,7 +828,16 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { new Right(new Json.Str(SafeNumbers.toString(a))) } - implicit val currency: JsonEncoder[java.util.Currency] = stringify(_.toString) + implicit val currency: JsonEncoder[java.util.Currency] = new JsonEncoder[java.util.Currency] { + def unsafeEncode(a: java.util.Currency, indent: Option[Int], out: Write): Unit = { + out.write('"') + out.write(a.toString) + out.write('"') + } + + override final def toJsonAST(a: java.util.Currency): Either[String, Json] = + new Right(new Json.Str(a.toString)) + } } private[json] trait EncoderLowPriority4 extends EncoderLowPriorityVersionSpecific { diff --git a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala index e454e0212..f1bd90c0c 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala @@ -95,10 +95,13 @@ final class FastCharSequence(s: Array[Char]) extends CharSequence { // java.io.StringReader uses a lock, which reduces perf by x2, this also allows // fast retraction and access to raw char arrays (which are faster than Strings) private[zio] final class FastStringReader(s: CharSequence) extends RetractReader with PlaybackReader { - private[this] var i: Int = 0 - def offset(): Int = i - private val len: Int = s.length - def close(): Unit = () + private[this] var i: Int = 0 + private[this] val len: Int = s.length + + def offset(): Int = i + + def close(): Unit = () + override def read(): Int = { i += 1 if (i > len) -1 @@ -111,10 +114,9 @@ private[zio] final class FastStringReader(s: CharSequence) extends RetractReader } override def nextNonWhitespace(): Char = { while ({ - { - i += 1 - if (i > len) throw new UnexpectedEnd - }; isWhitespace(s.charAt(i - 1)) + i += 1 + if (i > len) throw new UnexpectedEnd + isWhitespace(s.charAt(i - 1)) }) () s.charAt(i - 1) } From 2ed7733384205ff9b1f93ee870711180f851427e Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 21 Feb 2025 18:24:38 +0100 Subject: [PATCH 180/311] More efficient decoding of UUIDs (#1331) --- .../scala-2.x/zio/json/JsonFieldDecoder.scala | 4 +- .../scala-3/zio/json/JsonFieldDecoder.scala | 4 +- .../src/main/scala/zio/json/JsonDecoder.scala | 17 +- .../main/scala/zio/json/internal/lexer.scala | 148 ++++++++++++++++++ .../main/scala/zio/json/uuid/UUIDParser.scala | 139 ++++++++-------- .../src/test/scala/zio/json/DecoderSpec.scala | 64 ++++---- 6 files changed, 254 insertions(+), 122 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala index 0dc35e599..32936ae45 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala @@ -69,11 +69,11 @@ object JsonFieldDecoder extends LowPriorityJsonFieldDecoder { def unsafeDecodeField(trace: List[JsonError], in: String): java.util.UUID = try UUIDParser.unsafeParse(in) catch { - case _: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${strip(in)}", trace) + case _: IllegalArgumentException => Lexer.error("expected UUID string", trace) } } - // use this instead of `string.mapOrFail` in supertypes (to prevent class initialization error at runtime) + // FIXME: remove from the next major version private[json] def mapStringOrFail[A](f: String => Either[String, A]): JsonFieldDecoder[A] = new JsonFieldDecoder[A] { def unsafeDecodeField(trace: List[JsonError], in: String): A = diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala index 83e2d57ec..bacb87594 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala @@ -69,11 +69,11 @@ object JsonFieldDecoder extends LowPriorityJsonFieldDecoder { def unsafeDecodeField(trace: List[JsonError], in: String): java.util.UUID = try UUIDParser.unsafeParse(in) catch { - case _: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${strip(in)}", trace) + case _: IllegalArgumentException => Lexer.error("expected UUID string", trace) } } - // use this instead of `string.mapOrFail` in supertypes (to prevent class initialization error at runtime) + // FIXME: remove from the next major version private[json] def mapStringOrFail[A](f: String => Either[String, A]): JsonFieldDecoder[A] = new JsonFieldDecoder[A] { def unsafeDecodeField(trace: List[JsonError], in: String): A = diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index da2878218..e770dc5eb 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -910,19 +910,16 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } implicit val uuid: JsonDecoder[UUID] = new JsonDecoder[UUID] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): UUID = - parseUUID(trace, Lexer.string(trace, in).toString) + def unsafeDecode(trace: List[JsonError], in: RetractReader): UUID = Lexer.uuid(trace, in) override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID = json match { - case s: Json.Str => parseUUID(trace, s.value) - case _ => Lexer.error("expected string", trace) - } - - @inline private[this] def parseUUID(trace: List[JsonError], s: String): UUID = - try UUIDParser.unsafeParse(s) - catch { - case _: IllegalArgumentException => Lexer.error(s"Invalid UUID: ${strip(s)}", trace) + case s: Json.Str => + try UUIDParser.unsafeParse(s.value) + catch { + case _: IllegalArgumentException => Lexer.error("expected UUID string", trace) + } + case _ => Lexer.error("expected UUID string", trace) } } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 22decba0b..623454041 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -17,6 +17,7 @@ package zio.json.internal import zio.json.JsonDecoder.{ JsonError, UnsafeJson } +import java.util.UUID import scala.annotation._ // tries to stick to the spec, but maybe a bit loose in places (e.g. numbers) @@ -206,10 +207,157 @@ object Lexer { new String(cs, 0, i) } + def uuid(trace: List[JsonError], in: OneCharReader): UUID = { + var c = in.nextNonWhitespace() + if (c != '"') error("'\"'", c, trace) + var cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + if (c > 0xff) uuidError(trace) + if (i == cs.length) cs = java.util.Arrays.copyOf(cs, i << 1) + cs(i) = c + i += 1 + } + if ( + i == 36 && { + val c1 = cs(8) + val c2 = cs(13) + val c3 = cs(18) + val c4 = cs(23) + c1 == '-' && c2 == '-' && c3 == '-' && c4 == '-' + } + ) { + val ds = hexDigits + val msb1 = + ds(cs(0).toInt).toLong << 28 | + (ds(cs(1).toInt) << 24 | + ds(cs(2).toInt) << 20 | + ds(cs(3).toInt) << 16 | + ds(cs(4).toInt) << 12 | + ds(cs(5).toInt) << 8 | + ds(cs(6).toInt) << 4 | + ds(cs(7).toInt)) + val msb2 = + ds(cs(9).toInt) << 12 | + ds(cs(10).toInt) << 8 | + ds(cs(11).toInt) << 4 | + ds(cs(12).toInt) + val msb3 = + ds(cs(14).toInt) << 12 | + ds(cs(15).toInt) << 8 | + ds(cs(16).toInt) << 4 | + ds(cs(17).toInt) + val lsb1 = + ds(cs(19).toInt) << 12 | + ds(cs(20).toInt) << 8 | + ds(cs(21).toInt) << 4 | + ds(cs(22).toInt) + val lsb2 = + (ds(cs(24).toInt) << 16 | + ds(cs(25).toInt) << 12 | + ds(cs(26).toInt) << 8 | + ds(cs(27).toInt) << 4 | + ds(cs(28).toInt)).toLong << 28 | + (ds(cs(29).toInt) << 24 | + ds(cs(30).toInt) << 20 | + ds(cs(31).toInt) << 16 | + ds(cs(32).toInt) << 12 | + ds(cs(33).toInt) << 8 | + ds(cs(34).toInt) << 4 | + ds(cs(35).toInt)) + if ((msb1 | msb2 | msb3 | lsb1 | lsb2) >= 0L) { + return new UUID(msb1 << 32 | msb2.toLong << 16 | msb3, lsb1.toLong << 48 | lsb2) + } + } else if (i <= 36) { + return uuidExtended(trace, cs, i) + } + uuidError(trace) + } + + private[this] def uuidExtended(trace: List[JsonError], cs: Array[Char], len: Int): UUID = { + val dash1 = indexOfDash(cs, 1, len) + val dash2 = indexOfDash(cs, dash1 + 2, len) + val dash3 = indexOfDash(cs, dash2 + 2, len) + val dash4 = indexOfDash(cs, dash3 + 2, len) + if (dash4 >= 0) { + val ds = hexDigits + val section1 = uuidSection(trace, ds, cs, 0, dash1, 0xffffffff00000000L) + val section2 = uuidSection(trace, ds, cs, dash1 + 1, dash2, 0xffffffffffff0000L) + val section3 = uuidSection(trace, ds, cs, dash2 + 1, dash3, 0xffffffffffff0000L) + val section4 = uuidSection(trace, ds, cs, dash3 + 1, dash4, 0xffffffffffff0000L) + val section5 = uuidSection(trace, ds, cs, dash4 + 1, len, 0xffff000000000000L) + return new UUID((section1 << 32) | (section2 << 16) | section3, (section4 << 48) | section5) + } + uuidError(trace) + } + + private[this] def indexOfDash(cs: Array[Char], from: Int, to: Int): Int = { + var i = from + while (i < to) { + if (cs(i) == '-') return i + i += 1 + } + -1 + } + + private[this] def uuidSection( + trace: List[JsonError], + ds: Array[Byte], + cs: Array[Char], + from: Int, + to: Int, + mask: Long + ): Long = { + if (from < to && from + 16 >= to) { + var result = 0L + var i = from + while (i < to) { + result = (result << 4) | ds(cs(i).toInt) + i += 1 + } + if ((result & mask) == 0L) return result + } + uuidError(trace) + } + + @noinline private[this] def uuidError(trace: List[JsonError]): Nothing = error("expected UUID string", trace) + private[this] val charArrays = new ThreadLocal[Array[Char]] { override def initialValue(): Array[Char] = new Array[Char](1024) } + private[this] val hexDigits: Array[Byte] = { + val ns = new Array[Byte](256) + java.util.Arrays.fill(ns, -1: Byte) + ns('0') = 0 + ns('1') = 1 + ns('2') = 2 + ns('3') = 3 + ns('4') = 4 + ns('5') = 5 + ns('6') = 6 + ns('7') = 7 + ns('8') = 8 + ns('9') = 9 + ns('A') = 10 + ns('B') = 11 + ns('C') = 12 + ns('D') = 13 + ns('E') = 14 + ns('F') = 15 + ns('a') = 10 + ns('b') = 11 + ns('c') = 12 + ns('d') = 13 + ns('e') = 14 + ns('f') = 15 + ns + } + def char(trace: List[JsonError], in: OneCharReader): Char = { var c = in.nextNonWhitespace() if (c != '"') error("'\"'", c, trace) diff --git a/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala b/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala index c82120d21..372a160f2 100644 --- a/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala +++ b/zio-json/shared/src/main/scala/zio/json/uuid/UUIDParser.scala @@ -15,16 +15,13 @@ */ package zio.json.uuid -import scala.annotation.nowarn +import java.util.UUID import scala.util.control.NoStackTrace -// A port of https://github.com/openjdk/jdk/commit/ebadfaeb2e1cc7b5ce5f101cd8a539bc5478cf5b with optimizations applied private[json] object UUIDParser { - // Converts characters to their numeric representation (for example 'E' or 'e' becomes 0XE) - private[this] val CharToNumeric: Array[Byte] = { - // by filling in -1's we prevent from trying to parse invalid characters - val ns = Array.fill[Byte](256)(-1) - + private[this] val hexDigits: Array[Byte] = { + val ns = new Array[Byte](256) + java.util.Arrays.fill(ns, -1: Byte) ns('0') = 0 ns('1') = 1 ns('2') = 2 @@ -35,104 +32,92 @@ private[json] object UUIDParser { ns('7') = 7 ns('8') = 8 ns('9') = 9 - ns('A') = 10 ns('B') = 11 ns('C') = 12 ns('D') = 13 ns('E') = 14 ns('F') = 15 - ns('a') = 10 ns('b') = 11 ns('c') = 12 ns('d') = 13 ns('e') = 14 ns('f') = 15 - ns } - def unsafeParse(input: String): java.util.UUID = + def unsafeParse(input: String): java.util.UUID = { if ( - input.length != 36 || { + input.length == 36 && { val ch1 = input.charAt(8) val ch2 = input.charAt(13) val ch3 = input.charAt(18) val ch4 = input.charAt(23) - ch1 != '-' || ch2 != '-' || ch3 != '-' || ch4 != '-' + ch1 == '-' && ch2 == '-' && ch3 == '-' && ch4 == '-' + } + ) { + val ds = hexDigits + val msb1 = uuidNibble(ds, input, 0) + val msb2 = uuidNibble(ds, input, 4) + val msb3 = uuidNibble(ds, input, 9) + val msb4 = uuidNibble(ds, input, 14) + val lsb1 = uuidNibble(ds, input, 19) + val lsb2 = uuidNibble(ds, input, 24) + val lsb3 = uuidNibble(ds, input, 28) + val lsb4 = uuidNibble(ds, input, 32) + if ((msb1 | msb2 | msb3 | msb4 | lsb1 | lsb2 | lsb3 | lsb4) >= 0) { + return new UUID( + msb1.toLong << 48 | msb2.toLong << 32 | msb3.toLong << 16 | msb4, + lsb1.toLong << 48 | lsb2.toLong << 32 | lsb3.toLong << 16 | lsb4 + ) } - ) unsafeParseExtended(input) - else { - val ch2n = CharToNumeric - val msb1 = parseNibbles(ch2n, input, 0) - val msb2 = parseNibbles(ch2n, input, 4) - val msb3 = parseNibbles(ch2n, input, 9) - val msb4 = parseNibbles(ch2n, input, 14) - val lsb1 = parseNibbles(ch2n, input, 19) - val lsb2 = parseNibbles(ch2n, input, 24) - val lsb3 = parseNibbles(ch2n, input, 28) - val lsb4 = parseNibbles(ch2n, input, 32) - if ((msb1 | msb2 | msb3 | msb4 | lsb1 | lsb2 | lsb3 | lsb4) < 0) invalidUUIDError() - new java.util.UUID(msb1 << 48 | msb2 << 32 | msb3 << 16 | msb4, lsb1 << 48 | lsb2 << 32 | lsb3 << 16 | lsb4) + } else if (input.length <= 36) { + return uuidExtended(input) } - - // A nibble is 4 bits - @nowarn("msg=implicit numeric widening") - private[this] def parseNibbles(ch2n: Array[Byte], input: String, position: Int): Long = { - val ch1 = input.charAt(position) - val ch2 = input.charAt(position + 1) - val ch3 = input.charAt(position + 2) - val ch4 = input.charAt(position + 3) - if ((ch1 | ch2 | ch3 | ch4) > 0xff) -1L - else ch2n(ch1) << 12 | ch2n(ch2) << 8 | ch2n(ch3) << 4 | ch2n(ch4) + uuidError() } - private[this] def unsafeParseExtended(input: String): java.util.UUID = { - val len = input.length - if (len > 36) invalidUUIDError() - val dash1 = input.indexOf('-', 0) - val dash2 = input.indexOf('-', dash1 + 1) - val dash3 = input.indexOf('-', dash2 + 1) - val dash4 = input.indexOf('-', dash3 + 1) - val dash5 = input.indexOf('-', dash4 + 1) - - // For any valid input, dash1 through dash4 will be positive and dash5 will be negative, - // but it's enough to check dash4 and dash5: - // - if dash1 is -1, dash4 will be -1 - // - if dash1 is positive but dash2 is -1, dash4 will be -1 - // - if dash1 and dash2 is positive, dash3 will be -1, dash4 will be positive, but so will dash5 - if (dash4 < 0 || dash5 >= 0) invalidUUIDError() + private[this] def uuidNibble(ds: Array[Byte], input: String, offset: Int): Int = { + val ch1 = input.charAt(offset).toInt + val ch2 = input.charAt(offset + 1).toInt + val ch3 = input.charAt(offset + 2).toInt + val ch4 = input.charAt(offset + 3).toInt + if ((ch1 | ch2 | ch3 | ch4) > 0xff) -1 + else ds(ch1) << 12 | ds(ch2) << 8 | ds(ch3) << 4 | ds(ch4) + } - val ch2n = CharToNumeric - val section1 = parseSection(ch2n, input, 0, dash1, 0xfffffff00000000L) - val section2 = parseSection(ch2n, input, dash1 + 1, dash2, 0xfffffffffff0000L) - val section3 = parseSection(ch2n, input, dash2 + 1, dash3, 0xfffffffffff0000L) - val section4 = parseSection(ch2n, input, dash3 + 1, dash4, 0xfffffffffff0000L) - val section5 = parseSection(ch2n, input, dash4 + 1, len, 0xfff000000000000L) - new java.util.UUID((section1 << 32) | (section2 << 16) | section3, (section4 << 48) | section5) + private[this] def uuidExtended(input: String): UUID = { + val dash1 = input.indexOf('-', 1) + val dash2 = input.indexOf('-', dash1 + 2) + val dash3 = input.indexOf('-', dash2 + 2) + val dash4 = input.indexOf('-', dash3 + 2) + if (dash4 >= 0) { + val ds = hexDigits + val section1 = uuidSection(ds, input, 0, dash1, 0xffffffff00000000L) + val section2 = uuidSection(ds, input, dash1 + 1, dash2, 0xffffffffffff0000L) + val section3 = uuidSection(ds, input, dash2 + 1, dash3, 0xffffffffffff0000L) + val section4 = uuidSection(ds, input, dash3 + 1, dash4, 0xffffffffffff0000L) + val section5 = uuidSection(ds, input, dash4 + 1, input.length, 0xffff000000000000L) + return new UUID((section1 << 32) | (section2 << 16) | section3, (section4 << 48) | section5) + } + uuidError() } - @nowarn("msg=implicit numeric widening") - private[this] def parseSection( - ch2n: Array[Byte], - input: String, - beginIndex: Int, - endIndex: Int, - zeroMask: Long - ): Long = { - if (beginIndex >= endIndex || beginIndex + 16 < endIndex) invalidUUIDError() - var result = 0L - var i = beginIndex - while (i < endIndex) { - result = (result << 4) | ch2n(input.charAt(i)) - i += 1 + private[this] def uuidSection(ds: Array[Byte], input: String, from: Int, to: Int, mask: Long): Long = { + if (from < to && from + 16 >= to) { + var result = 0L + var i = from + while (i < to) { + val c = input.charAt(i).toInt + if (c > 0xff) uuidError() + result = (result << 4) | ds(c) + i += 1 + } + if ((result & mask) == 0L) return result } - if ((result & zeroMask) != 0) invalidUUIDError() - result + uuidError() } - @noinline - private[this] def invalidUUIDError(): Nothing = - throw new IllegalArgumentException with NoStackTrace + @noinline private[this] def uuidError(): Nothing = throw new IllegalArgumentException with NoStackTrace } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 05fbb9d81..d22a683a1 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -481,6 +481,8 @@ object DecoderSpec extends ZIOSpecDefault { val bad5 = """{"64d7c38d-2afd-XXXX-9832-4e70afe4b0f8": "value"}""" val bad6 = """{"64d7c38d-2afd-X-9832-4e70afe4b0f8": "value"}""" val bad7 = """{"0-0-0-0-00000000000000000": "value"}""" + val bad8 = """{"64d7c38d-2аfd-4514-9832-4e70afe4b0f8": "value"}""" + val bad9 = """{"0000000064D7C38D-FD-14-32-70АFE4B0f8": "value"}""" assert(ok1.fromJson[Map[UUID, String]])( isRight(equalTo(expectedMap("64d7c38d-2afd-4514-9832-4e70afe4b0f8"))) @@ -491,23 +493,15 @@ object DecoderSpec extends ZIOSpecDefault { assert(ok3.fromJson[Map[UUID, String]])( isRight(equalTo(expectedMap("00000000-0000-0000-0000-000000000000"))) ) && - assert(bad1.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: "))) && - assert(bad2.fromJson[Map[UUID, String]])( - isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-9832-4e70afe4b0f80")) - ) && - assert(bad3.fromJson[Map[UUID, String]])( - isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80")) - ) && - assert(bad4.fromJson[Map[UUID, String]])( - isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8")) - ) && - assert(bad5.fromJson[Map[UUID, String]])( - isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8")) - ) && - assert(bad6.fromJson[Map[UUID, String]])( - isLeft(containsString("Invalid UUID: 64d7c38d-2afd-X-9832-4e70afe4b0f8")) - ) && - assert(bad7.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 0-0-0-0-00000000000000000"))) + assert(bad1.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && + assert(bad2.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && + assert(bad3.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && + assert(bad4.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && + assert(bad5.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && + assert(bad6.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && + assert(bad7.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && + assert(bad8.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && + assert(bad9.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) }, test("zio.Chunk") { val jsonStr = """["5XL","2XL","XL"]""" @@ -537,17 +531,21 @@ object DecoderSpec extends ZIOSpecDefault { val bad5 = """"64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"""" val bad6 = """"64d7c38d-2afd-X-9832-4e70afe4b0f8"""" val bad7 = """"0-0-0-0-00000000000000000"""" + val bad8 = """"64d7c38d-2аfd-4514-9832-4e70afe4b0f8"""" + val bad9 = """"0000000064D7C38D-FD-14-32-70АFE4B0f8"""" assert(ok1.fromJson[UUID])(isRight(equalTo(UUID.fromString("64d7c38d-2afd-4514-9832-4e70afe4b0f8")))) && assert(ok2.fromJson[UUID])(isRight(equalTo(UUID.fromString("64D7C38D-00FD-0014-0032-0070AfE4B0f8")))) && assert(ok3.fromJson[UUID])(isRight(equalTo(UUID.fromString("00000000-0000-0000-0000-000000000000")))) && - assert(bad1.fromJson[UUID])(isLeft(containsString("Invalid UUID: "))) && - assert(bad2.fromJson[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-9832-4e70afe4b0f80"))) && - assert(bad3.fromJson[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80"))) && - assert(bad4.fromJson[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8"))) && - assert(bad5.fromJson[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"))) && - assert(bad6.fromJson[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-X-9832-4e70afe4b0f8"))) && - assert(bad7.fromJson[UUID])(isLeft(containsString("Invalid UUID: 0-0-0-0-00000000000000000"))) + assert(bad1.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad2.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad3.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad4.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad5.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad6.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad7.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad8.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad9.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) }, test("java.util.Currency") { assert(""""USD"""".fromJson[java.util.Currency])(isRight(equalTo(java.util.Currency.getInstance("USD")))) && @@ -818,17 +816,21 @@ object DecoderSpec extends ZIOSpecDefault { val bad5 = Json.Str("64d7c38d-2afd-XXXX-9832-4e70afe4b0f8") val bad6 = Json.Str("64d7c38d-2afd-X-9832-4e70afe4b0f8") val bad7 = Json.Str("0-0-0-0-00000000000000000") + val bad8 = Json.Str("64d7c38d-2аfd-4514-9832-4e70afe4b0f8") + val bad9 = Json.Str("0000000064D7C38D-FD-14-32-70АFE4B0f8") assert(ok1.as[UUID])(isRight(equalTo(UUID.fromString("64d7c38d-2afd-4514-9832-4e70afe4b0f8")))) && assert(ok2.as[UUID])(isRight(equalTo(UUID.fromString("64D7C38D-00FD-0014-0032-0070AFE4B0f8")))) && assert(ok3.as[UUID])(isRight(equalTo(UUID.fromString("00000000-0000-0000-0000-000000000000")))) && - assert(bad1.as[UUID])(isLeft(containsString("Invalid UUID: "))) && - assert(bad2.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-9832-4e70afe4b0f80"))) && - assert(bad3.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80"))) && - assert(bad4.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8"))) && - assert(bad5.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"))) && - assert(bad6.as[UUID])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-X-9832-4e70afe4b0f8"))) && - assert(bad7.as[UUID])(isLeft(containsString("Invalid UUID: 0-0-0-0-00000000000000000"))) + assert(bad1.as[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad2.as[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad3.as[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad4.as[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad5.as[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad6.as[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad7.as[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad8.as[UUID])(isLeft(containsString("(expected UUID string)"))) && + assert(bad9.as[UUID])(isLeft(containsString("(expected UUID string)"))) } ) ) From 0783dfc953b1e2151fabf4c0e9bef51623842536 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 21 Feb 2025 22:41:50 +0100 Subject: [PATCH 181/311] Update sbt-scoverage to 2.3.1 (#1332) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 2b7c51655..a5f13021b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -9,7 +9,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.31") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.9" From cb5d1aa3a8e26959f3681dae984175392577527f Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 22 Feb 2025 09:24:45 +0100 Subject: [PATCH 182/311] More efficient reading of the next character (#1333) --- .../scala/zio/json/internal/readers.scala | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala index f1bd90c0c..3d68052b7 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala @@ -95,30 +95,37 @@ final class FastCharSequence(s: Array[Char]) extends CharSequence { // java.io.StringReader uses a lock, which reduces perf by x2, this also allows // fast retraction and access to raw char arrays (which are faster than Strings) private[zio] final class FastStringReader(s: CharSequence) extends RetractReader with PlaybackReader { - private[this] var i: Int = 0 - private[this] val len: Int = s.length + private[this] var i: Int = 0 def offset(): Int = i def close(): Unit = () override def read(): Int = { - i += 1 - if (i > len) -1 - else s.charAt(i - 1).toInt // -1 is faster than assigning a temp value + if (i < s.length) { + val c = s.charAt(i) + i += 1 + return c.toInt + } + -1 } + override def readChar(): Char = { - i += 1 - if (i > len) throw new UnexpectedEnd - s.charAt(i - 1) + if (i < s.length) { + val c = s.charAt(i) + i += 1 + return c + } + throw new UnexpectedEnd } + override def nextNonWhitespace(): Char = { - while ({ + while (i < s.length) { + val c = s.charAt(i) i += 1 - if (i > len) throw new UnexpectedEnd - isWhitespace(s.charAt(i - 1)) - }) () - s.charAt(i - 1) + if (c != ' ' && c != '\n' && (c | 0x4) != '\r') return c + } + throw new UnexpectedEnd } def retract(): Unit = i -= 1 From 65ac7289fd5ec07a552c9d008c012e04fd0737c1 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 22 Feb 2025 17:05:30 +0100 Subject: [PATCH 183/311] More efficient pretty printing (#1334) --- .../shared/src/main/scala/zio/json/JsonEncoder.scala | 9 +++++++-- .../src/main/scala/zio/json/internal/readers.scala | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 1cad8acc8..14204da23 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -323,9 +323,14 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with def pad(indent: Option[Int], out: Write): Unit = if (indent ne None) { out.write('\n') - var i = indent.get + var i = indent.get + val ws = 8224: Short + while (i > 4) { + out.write(ws, ws, ws, ws) + i -= 4 + } while (i > 0) { - out.write(' ', ' ') + out.write(ws) i -= 1 } } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala index 3d68052b7..92691b2f0 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala @@ -103,9 +103,9 @@ private[zio] final class FastStringReader(s: CharSequence) extends RetractReader override def read(): Int = { if (i < s.length) { - val c = s.charAt(i) + val c = s.charAt(i).toInt i += 1 - return c.toInt + return c } -1 } From 358c6aea7bc0ae4db6519e43f3e892983c3014a1 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 23 Feb 2025 09:48:07 +0100 Subject: [PATCH 184/311] More efficient implementation of `FastStringReader` (#1335) --- .../scala/zio/json/internal/readers.scala | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala index 92691b2f0..c6d493103 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala @@ -95,35 +95,41 @@ final class FastCharSequence(s: Array[Char]) extends CharSequence { // java.io.StringReader uses a lock, which reduces perf by x2, this also allows // fast retraction and access to raw char arrays (which are faster than Strings) private[zio] final class FastStringReader(s: CharSequence) extends RetractReader with PlaybackReader { - private[this] var i: Int = 0 + private[this] var i: Int = 0 + private[this] val len: Int = s.length def offset(): Int = i def close(): Unit = () override def read(): Int = { - if (i < s.length) { - val c = s.charAt(i).toInt - i += 1 - return c + val i = this.i + if (i < len) { + this.i = i + 1 + return s.charAt(i).toInt } -1 } override def readChar(): Char = { - if (i < s.length) { - val c = s.charAt(i) - i += 1 - return c + val i = this.i + if (i < len) { + this.i = i + 1 + return s.charAt(i) } throw new UnexpectedEnd } override def nextNonWhitespace(): Char = { - while (i < s.length) { + var i = this.i + val len = this.len + while (i < len) { val c = s.charAt(i) i += 1 - if (c != ' ' && c != '\n' && (c | 0x4) != '\r') return c + if (c != ' ' && c != '\n' && (c | 0x4) != '\r') { + this.i = i + return c + } } throw new UnexpectedEnd } From 4089393c616613b2c1975dee995748ef2a2d6283 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 23 Feb 2025 19:03:43 +0100 Subject: [PATCH 185/311] More efficient implementation of `WithRecordingReader` (#1336) --- .../main/scala/zio/json/internal/lexer.scala | 12 ++- .../scala/zio/json/internal/readers.scala | 85 ++++++++++--------- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 623454041..9594b80ba 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -38,14 +38,12 @@ object Lexer { @noinline private[json] def error(c: Char, trace: List[JsonError]): Nothing = error(s"invalid '\\$c' in string", trace) - // True if we got a string (implies a retraction), False for } + // FIXME: remove trace paramenter in the next major version + // True if we got anything besides a }, False for } @inline def firstField(trace: List[JsonError], in: RetractReader): Boolean = - (in.nextNonWhitespace(): @switch) match { - case '"' => - in.retract() - true - case '}' => false - case c => error("string or '}'", c, trace) + in.nextNonWhitespace() != '}' && { + in.retract() + true } // True if we got a comma, and False for } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala index c6d493103..9328fa30e 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala @@ -121,8 +121,7 @@ private[zio] final class FastStringReader(s: CharSequence) extends RetractReader } override def nextNonWhitespace(): Char = { - var i = this.i - val len = this.len + var i = this.i while (i < len) { val c = s.charAt(i) i += 1 @@ -131,6 +130,7 @@ private[zio] final class FastStringReader(s: CharSequence) extends RetractReader return c } } + this.i = i throw new UnexpectedEnd } @@ -175,8 +175,7 @@ private[zio] sealed trait RecordingReader extends RetractReader { def rewind(): Unit } private[zio] object RecordingReader { - def apply(in: OneCharReader): RecordingReader = - new WithRecordingReader(in, 64) + @inline def apply(in: OneCharReader): RecordingReader = new WithRecordingReader(in, 64) } // used to optimise RecordingReader @@ -195,66 +194,70 @@ private[zio] sealed trait PlaybackReader extends OneCharReader { private[zio] final class WithRecordingReader(in: OneCharReader, initial: Int) extends RecordingReader with PlaybackReader { + private[this] var state: Int = 0 // -1: neither recording nor replaying, 0: recording, 1: replaying private[this] var tape: Array[Char] = new Array(Math.max(initial, 1)) - private[this] var eob: Int = -1 + private[this] var reading: Int = 0 private[this] var writing: Int = 0 - private[this] var reading: Int = -1 def close(): Unit = in.close() override def read(): Int = - try readChar().toInt - catch { - case _: UnexpectedEnd => - eob = reading - -1 + if (state < 0) in.read() + else if (state > 0) { + var reading = this.reading + val c = tape(reading).toInt + reading += 1 + this.reading = reading + if (reading == writing) state = -1 // chatch up, stop replaying + c + } else { + val writing = this.writing + if (writing == tape.length) tape = Arrays.copyOf(tape, writing << 1) + val c = in.read() + if (c >= 0) { + tape(writing) = c.toChar + this.writing = writing + 1 + } + c } + override def readChar(): Char = - if (reading != -1) { - if (reading == eob) throw new UnexpectedEnd - val v = tape(reading) + if (state < 0) in.readChar() + else if (state > 0) { + var reading = this.reading + val c = tape(reading) reading += 1 - if (reading >= writing) { - reading = -1 // caught up - writing = -1 // stop recording - } - v + this.reading = reading + if (reading == writing) state = -1 // chatch up, stop replaying + c } else { - val v = in.readChar() - if (writing != -1) { - tape(writing) = v - writing += 1 - if (writing == tape.length) - tape = Arrays.copyOf(tape, tape.length << 1) - } - v + val writing = this.writing + if (writing == tape.length) tape = Arrays.copyOf(tape, writing << 1) + val c = in.readChar() + tape(writing) = c + this.writing = writing + 1 + c } def rewind(): Unit = - if (writing != -1) - reading = 0 + if (state == 0) state = 1 // start replaying else throw new RewindTwice def retract(): Unit = - if (reading == -1) { + if (state > 0) reading -= 1 + else { in match { case rr: RetractReader => rr.retract() - if (writing != -1) { - writing -= 1 // factor in retracted delegate - } - + if (state == 0) writing -= 1 // factor in retracted delegate case _ => - reading = writing - 1 + throw new UnsupportedOperationException("underlying reader does not support retract") } - } else - reading -= 1 + } def offset(): Int = - if (reading == -1) - writing - else - reading + if (state > 0) reading + else writing def history(idx: Int): Char = tape(idx) } From d5be01afb81d45c45eaca064297040aa6569328b Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 24 Feb 2025 07:13:29 +0100 Subject: [PATCH 186/311] Update scalafmt-core to 3.9.1 (#1337) --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 1e7307b1e..70063b384 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.9.0" +version = "3.9.1" runner.dialect = scala213 maxColumn = 120 align.preset = most From 80adab871e22601d888ffd3bdb42ce8cdf5aef1e Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 24 Feb 2025 16:04:26 +0100 Subject: [PATCH 187/311] More efficient decoding of booleans (#1338) --- .../main/scala/zio/json/internal/lexer.scala | 103 ++++++++---------- 1 file changed, 45 insertions(+), 58 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 9594b80ba..a504c9096 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -41,38 +41,38 @@ object Lexer { // FIXME: remove trace paramenter in the next major version // True if we got anything besides a }, False for } @inline def firstField(trace: List[JsonError], in: RetractReader): Boolean = - in.nextNonWhitespace() != '}' && { + if (in.nextNonWhitespace() != '}') { in.retract() true - } + } else false // True if we got a comma, and False for } - @inline def nextField(trace: List[JsonError], in: OneCharReader): Boolean = - (in.nextNonWhitespace(): @switch) match { - case ',' => true - case '}' => false - case c => error("',' or '}'", c, trace) - } + @inline def nextField(trace: List[JsonError], in: OneCharReader): Boolean = { + val c = in.nextNonWhitespace() + if (c == ',') true + else if (c == '}') false + else error("',' or '}'", c, trace) + } // True if we got anything besides a ], False for ] @inline def firstArrayElement(in: RetractReader): Boolean = - in.nextNonWhitespace() != ']' && { + if (in.nextNonWhitespace() != ']') { in.retract() true - } + } else false - @inline def nextArrayElement(trace: List[JsonError], in: OneCharReader): Boolean = - (in.nextNonWhitespace(): @switch) match { - case ',' => true - case ']' => false - case c => error("',' or ']'", c, trace) - } + @inline def nextArrayElement(trace: List[JsonError], in: OneCharReader): Boolean = { + val c = in.nextNonWhitespace() + if (c == ',') true + else if (c == ']') false + else error("',' or ']'", c, trace) + } @inline def field(trace: List[JsonError], in: OneCharReader, matrix: StringMatrix): Int = { val f = enumeration(trace, in, matrix) val c = in.nextNonWhitespace() - if (c != ':') error("':'", c, trace) - f + if (c == ':') return f + error("':'", c, trace) } def enumeration(trace: List[JsonError], in: OneCharReader, matrix: StringMatrix): Int = { @@ -89,8 +89,7 @@ object Lexer { bs = matrix.update(bs, i, c) i += 1 } - bs = matrix.exact(bs, i) - matrix.first(bs) + matrix.first(matrix.exact(bs, i)) } @noinline def skipValue(trace: List[JsonError], in: RetractReader): Unit = @@ -167,14 +166,14 @@ object Lexer { case 'n' => '\n' case 'r' => '\r' case 't' => '\t' - case 'u' => Lexer.nextHex4(trace, in) - case c => Lexer.error(c, trace) + case 'u' => nextHex4(trace, in) + case c => error(c, trace) }).toInt } else if (c == '\\') { escaped = true read() } else if (c == '"') -1 // this is the EOS for the caller - else if (c < ' ') Lexer.error("invalid control in string", trace) + else if (c < ' ') error("invalid control in string", trace) else c.toInt } @@ -208,15 +207,14 @@ object Lexer { def uuid(trace: List[JsonError], in: OneCharReader): UUID = { var c = in.nextNonWhitespace() if (c != '"') error("'\"'", c, trace) - var cs = charArrays.get + val cs = charArrays.get var i = 0 while ({ c = in.readChar() c != '"' }) { if (c == '\\') c = nextEscaped(trace, in) - if (c > 0xff) uuidError(trace) - if (i == cs.length) cs = java.util.Arrays.copyOf(cs, i << 1) + if (i == 36 || c > 0xff) uuidError(trace) cs(i) = c i += 1 } @@ -240,20 +238,20 @@ object Lexer { ds(cs(6).toInt) << 4 | ds(cs(7).toInt)) val msb2 = - ds(cs(9).toInt) << 12 | + (ds(cs(9).toInt) << 12 | ds(cs(10).toInt) << 8 | ds(cs(11).toInt) << 4 | - ds(cs(12).toInt) + ds(cs(12).toInt)).toLong val msb3 = - ds(cs(14).toInt) << 12 | + (ds(cs(14).toInt) << 12 | ds(cs(15).toInt) << 8 | ds(cs(16).toInt) << 4 | - ds(cs(17).toInt) + ds(cs(17).toInt)).toLong val lsb1 = - ds(cs(19).toInt) << 12 | + (ds(cs(19).toInt) << 12 | ds(cs(20).toInt) << 8 | ds(cs(21).toInt) << 4 | - ds(cs(22).toInt) + ds(cs(22).toInt)).toLong val lsb2 = (ds(cs(24).toInt) << 16 | ds(cs(25).toInt) << 12 | @@ -268,7 +266,7 @@ object Lexer { ds(cs(34).toInt) << 4 | ds(cs(35).toInt)) if ((msb1 | msb2 | msb3 | lsb1 | lsb2) >= 0L) { - return new UUID(msb1 << 32 | msb2.toLong << 16 | msb3, lsb1.toLong << 48 | lsb2) + return new UUID(msb1 << 32 | msb2 << 16 | msb3, lsb1 << 48 | lsb2) } } else if (i <= 36) { return uuidExtended(trace, cs, i) @@ -325,7 +323,7 @@ object Lexer { @noinline private[this] def uuidError(trace: List[JsonError]): Nothing = error("expected UUID string", trace) private[this] val charArrays = new ThreadLocal[Array[Char]] { - override def initialValue(): Array[Char] = new Array[Char](1024) + override def initialValue(): Array[Char] = new Array[Char](1024) // should be longer than 256 } private[this] val hexDigits: Array[Byte] = { @@ -384,37 +382,26 @@ object Lexer { case c => error(c, trace) } - def nextHex4(trace: List[JsonError], in: OneCharReader): Char = { + private[this] def nextHex4(trace: List[JsonError], in: OneCharReader): Char = { var i, accum = 0 while (i < 4) { - val c = in.readChar() - accum <<= 4 - accum += { - if ('0' <= c && c <= '9') c - '0' - else if ('A' <= c && c <= 'F') c - 'A' + 10 - else if ('a' <= c && c <= 'f') c - 'a' + 10 - else error("invalid charcode in string", trace) - } + val c = in.readChar() | 0x20 + accum = (accum << 4) + c i += 1 + if ('0' <= c && c <= '9') accum -= 48 + else if ('a' <= c && c <= 'f') accum -= 87 + else error("invalid charcode in string", trace) } accum.toChar } - def boolean(trace: List[JsonError], in: OneCharReader): Boolean = - (in.nextNonWhitespace(): @switch) match { - case 't' => - if (in.readChar() != 'r' || in.readChar() != 'u' || in.readChar() != 'e') { - error("expected 'true'", trace) - } - true - case 'f' => - if (in.readChar() != 'a' || in.readChar() != 'l' || in.readChar() != 's' || in.readChar() != 'e') { - error("expected 'false'", trace) - } - false - case c => - error("'true' or 'false'", c, trace) - } + def boolean(trace: List[JsonError], in: OneCharReader): Boolean = { + val c = in.nextNonWhitespace() + if (c == 't' && in.readChar() == 'r' && in.readChar() == 'u' && in.readChar() == 'e') true + else if (c == 'f' && in.readChar() == 'a' && in.readChar() == 'l' && in.readChar() == 's' && in.readChar() == 'e') + false + else error("expected a Boolean", c, trace) + } def byte(trace: List[JsonError], in: RetractReader): Byte = try { From d3a2f3aa7786b87d32d97b102481885c6cc89bd7 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 24 Feb 2025 20:20:36 +0100 Subject: [PATCH 188/311] Add a compile-time option to toggle serialization of enum values and sealed trait's case objects as JSON strings or JSON objects (#1339) --- .../zio/json/JsonCodecConfiguration.scala | 188 ++++++++++++++++++ .../src/main/scala-2.x/zio/json/macros.scala | 94 ++++++--- .../zio/json/JsonCodecConfiguration.scala | 75 ++++++- .../src/main/scala-3/zio/json/macros.scala | 176 +++++++++------- .../zio/json/CodecVersionSpecificSpec.scala | 3 +- .../zio/json/DecoderVersionSpecificSpec.scala | 4 +- .../zio/json/EncoderVesionSpecificSpec.scala | 3 +- .../zio/json/CodecVersionSpecificSpec.scala | 29 ++- .../zio/json/DecoderVersionSpecificSpec.scala | 109 +++++++++- .../scala-3/zio/json/DerivedCodecSpec.scala | 36 ---- .../scala-3/zio/json/DerivedDecoderSpec.scala | 91 --------- .../scala-3/zio/json/DerivedEncoderSpec.scala | 69 ------- .../zio/json/EncoderVesionSpecificSpec.scala | 68 ++++++- .../src/test/scala/zio/json/DecoderSpec.scala | 44 ++++ .../src/test/scala/zio/json/EncoderSpec.scala | 46 +++++ 15 files changed, 721 insertions(+), 314 deletions(-) create mode 100644 zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecConfiguration.scala rename zio-json/shared/src/main/{scala => scala-3}/zio/json/JsonCodecConfiguration.scala (66%) delete mode 100644 zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala delete mode 100644 zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala delete mode 100644 zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecConfiguration.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecConfiguration.scala new file mode 100644 index 000000000..e59274966 --- /dev/null +++ b/zio-json/shared/src/main/scala-2.x/zio/json/JsonCodecConfiguration.scala @@ -0,0 +1,188 @@ +package zio.json + +import zio.json.JsonCodecConfiguration.SumTypeHandling +import zio.json.JsonCodecConfiguration.SumTypeHandling.WrapperWithClassNameField + +/** + * When disabled for decoding, keys with empty collections will be omitted from the JSON. When disabled for encoding, + * missing keys will default to empty collections. + */ +case class ExplicitEmptyCollections(encoding: Boolean = true, decoding: Boolean = true) + +/** + * Implicit codec derivation configuration. + * + * @param sumTypeHandling + * see [[jsonDiscriminator]] + * @param fieldNameMapping + * see [[jsonMemberNames]] + * @param allowExtraFields + * see [[jsonNoExtraFields]] + * @param sumTypeMapping + * see [[jsonHintNames]] + * @param explicitNulls + * turns on explicit serialization of optional fields with None values + * @param explicitEmptyCollections + * turns on explicit serialization of fields with empty collections + * @param enumValuesAsStrings + * turns on serialization of enum values and sealed trait's case objects as strings + */ +final case class JsonCodecConfiguration( + sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, + fieldNameMapping: JsonMemberFormat = IdentityFormat, + allowExtraFields: Boolean = true, + sumTypeMapping: JsonMemberFormat = IdentityFormat, + explicitNulls: Boolean = false, + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections(), + enumValuesAsStrings: Boolean = false +) { + def this( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = this( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + false + ) + + def this( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = this( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + ExplicitEmptyCollections(), + false + ) + + def copy( + sumTypeHandling: SumTypeHandling = WrapperWithClassNameField.asInstanceOf[SumTypeHandling], + fieldNameMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], + allowExtraFields: Boolean = true, + sumTypeMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], + explicitNulls: Boolean = false, + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections(), + enumValuesAsStrings: Boolean = false + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + enumValuesAsStrings + ) + + def copy( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + this.enumValuesAsStrings + ) + + def copy( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + this.explicitEmptyCollections, + this.enumValuesAsStrings + ) +} + +object JsonCodecConfiguration { + def apply( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + false + ) + + def apply( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + ExplicitEmptyCollections(), + false + ) + + implicit val default: JsonCodecConfiguration = JsonCodecConfiguration() + + sealed trait SumTypeHandling { + def discriminatorField: Option[String] + } + + object SumTypeHandling { + + /** + * Use an object with a single key that is the class name. + */ + case object WrapperWithClassNameField extends SumTypeHandling { + override def discriminatorField: Option[String] = None + } + + /** + * For sealed classes, will determine the name of the field for disambiguating classes. + * + * The default is to not use a typehint field and instead have an object with a single key that is the class name. + * See [[WrapperWithClassNameField]]. + * + * Note that using a discriminator is less performant, uses more memory, and may be prone to DOS attacks that are + * impossible with the default encoding. In addition, there is slightly less type safety when using custom product + * encoders (which must write an unenforced object type). Only use this option if you must model an externally + * defined schema. + */ + final case class DiscriminatorField(name: String) extends SumTypeHandling { + override def discriminatorField: Option[String] = Some(name) + } + } +} diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index c170572eb..fb29986e5 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -196,6 +196,25 @@ final class jsonNoExtraFields extends Annotation */ final class jsonExclude extends Annotation +private class CaseObjectDecoder[Typeclass[_], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) + extends CollectionJsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + if (no_extra) { + Lexer.char(trace, in, '{') + Lexer.char(trace, in, '}') + } else Lexer.skipValue(trace, in) + ctx.rawConstruct(Nil) + } + + override def unsafeDecodeMissing(trace: List[JsonError]): A = ctx.rawConstruct(Nil) + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case _: Json.Obj | Json.Null => ctx.rawConstruct(Nil) + case _ => Lexer.error("expected object", trace) + } +} + object DeriveJsonDecoder { type Typeclass[A] = JsonDecoder[A] @@ -212,25 +231,7 @@ object DeriveJsonDecoder { }.isDefined || !config.allowExtraFields if (ctx.parameters.isEmpty) - new CollectionJsonDecoder[A] { - override def unsafeDecodeMissing(trace: List[JsonError]): A = ctx.rawConstruct(Nil) - - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - if (no_extra) { - Lexer.char(trace, in, '{') - Lexer.char(trace, in, '}') - } else { - Lexer.skipValue(trace, in) - } - ctx.rawConstruct(Nil) - } - - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case _: Json.Obj | Json.Null => ctx.rawConstruct(Nil) - case _ => Lexer.error("expected object", trace) - } - } + new CaseObjectDecoder(ctx, no_extra) else new CollectionJsonDecoder[A] { private[this] val (names, aliases): (Array[String], Array[(String, Int)]) = { @@ -403,10 +404,31 @@ object DeriveJsonDecoder { lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap = names.zipWithIndex.toMap - def discrim = + val isEnumeration = config.enumValuesAsStrings && + ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[JsonDecoder, _]]) + + val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - if (discrim.isEmpty) { + if (isEnumeration && discrim.isEmpty) { + new JsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val idx = Lexer.enumeration(trace, in, matrix) + if (idx != -1) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + else Lexer.error("invalid enumeration value", trace) + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case s: Json.Str => + namesMap.get(s.value) match { + case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + case _ => Lexer.error("invalid enumeration value", trace) + } + case _ => Lexer.error("expected string", trace) + } + } + } else if (discrim.isEmpty) { // We're not allowing extra fields in this encoding new JsonDecoder[A] { private[this] val spans = names.map(JsonError.ObjectAccess) @@ -481,17 +503,19 @@ object DeriveJsonDecoder { } object DeriveJsonEncoder { + private lazy val caseObjectEncoder = new JsonEncoder[Any] { + override def isEmpty(a: Any): Boolean = true + + def unsafeEncode(a: Any, indent: Option[Int], out: Write): Unit = out.write("{}") + + override final def toJsonAST(a: Any): Either[String, Json] = new Right(Json.Obj.empty) + } + type Typeclass[A] = JsonEncoder[A] def join[A](ctx: CaseClass[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = if (ctx.parameters.isEmpty) - new JsonEncoder[A] { - override def isEmpty(a: A): Boolean = true - - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = out.write("{}") - - override final def toJsonAST(a: A): Either[String, Json] = new Right(Json.Obj.empty) - } + caseObjectEncoder.narrow[A] else new JsonEncoder[A] { private[this] val (transformNames, nameTransform): (Boolean, String => String) = @@ -584,6 +608,8 @@ object DeriveJsonEncoder { } def split[A](ctx: SealedTrait[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = { + val isEnumeration = config.enumValuesAsStrings && + ctx.subtypes.forall(_.typeclass == caseObjectEncoder) val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val names: Array[String] = ctx.subtypes.map { p => @@ -592,7 +618,17 @@ object DeriveJsonEncoder { val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - if (discrim.isEmpty) { + if (isEnumeration && discrim.isEmpty) { + new JsonEncoder[A] { + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.split(a) { sub => + JsonEncoder.string.unsafeEncode(names(sub.index), indent, out) + } + + override final def toJsonAST(a: A): Either[String, Json] = ctx.split(a) { sub => + new Right(new Json.Str(names(sub.index))) + } + } + } else if (discrim.isEmpty) { new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.split(a) { sub => out.write('{') diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecConfiguration.scala similarity index 66% rename from zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala rename to zio-json/shared/src/main/scala-3/zio/json/JsonCodecConfiguration.scala index eaa1d512a..4765f3a1c 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodecConfiguration.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecConfiguration.scala @@ -20,6 +20,12 @@ case class ExplicitEmptyCollections(encoding: Boolean = true, decoding: Boolean * see [[jsonNoExtraFields]] * @param sumTypeMapping * see [[jsonHintNames]] + * @param explicitNulls + * turns on explicit serialization of optional fields with None values + * @param explicitEmptyCollections + * turns on explicit serialization of fields with empty collections + * @param enumValuesAsStrings + * turns on serialization of enum values and sealed trait's case objects as strings */ final case class JsonCodecConfiguration( sumTypeHandling: SumTypeHandling = WrapperWithClassNameField, @@ -27,8 +33,26 @@ final case class JsonCodecConfiguration( allowExtraFields: Boolean = true, sumTypeMapping: JsonMemberFormat = IdentityFormat, explicitNulls: Boolean = false, - explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections() + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections(), + enumValuesAsStrings: Boolean = true ) { + def this( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = this( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + true + ) + def this( sumTypeHandling: SumTypeHandling, fieldNameMapping: JsonMemberFormat, @@ -41,7 +65,8 @@ final case class JsonCodecConfiguration( allowExtraFields, sumTypeMapping, explicitNulls, - ExplicitEmptyCollections() + ExplicitEmptyCollections(), + true ) def copy( @@ -50,14 +75,33 @@ final case class JsonCodecConfiguration( allowExtraFields: Boolean = true, sumTypeMapping: JsonMemberFormat = IdentityFormat.asInstanceOf[JsonMemberFormat], explicitNulls: Boolean = false, - explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections() + explicitEmptyCollections: ExplicitEmptyCollections = ExplicitEmptyCollections(), + enumValuesAsStrings: Boolean = true ) = new JsonCodecConfiguration( sumTypeHandling, fieldNameMapping, allowExtraFields, sumTypeMapping, explicitNulls, - explicitEmptyCollections + explicitEmptyCollections, + enumValuesAsStrings + ) + + def copy( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + this.enumValuesAsStrings ) def copy( @@ -72,11 +116,29 @@ final case class JsonCodecConfiguration( allowExtraFields, sumTypeMapping, explicitNulls, - this.explicitEmptyCollections + this.explicitEmptyCollections, + this.enumValuesAsStrings ) } object JsonCodecConfiguration { + def apply( + sumTypeHandling: SumTypeHandling, + fieldNameMapping: JsonMemberFormat, + allowExtraFields: Boolean, + sumTypeMapping: JsonMemberFormat, + explicitNulls: Boolean, + explicitEmptyCollections: ExplicitEmptyCollections + ) = new JsonCodecConfiguration( + sumTypeHandling, + fieldNameMapping, + allowExtraFields, + sumTypeMapping, + explicitNulls, + explicitEmptyCollections, + true + ) + def apply( sumTypeHandling: SumTypeHandling, fieldNameMapping: JsonMemberFormat, @@ -89,7 +151,8 @@ object JsonCodecConfiguration { allowExtraFields, sumTypeMapping, explicitNulls, - ExplicitEmptyCollections() + ExplicitEmptyCollections(), + true ) implicit val default: JsonCodecConfiguration = JsonCodecConfiguration() diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index c220a800b..db41270cf 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -6,7 +6,6 @@ import scala.deriving.Mirror import scala.compiletime.* import scala.reflect.* import zio.Chunk - import zio.json.JsonDecoder.JsonError import zio.json.ast.Json import zio.json.internal.{ FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } @@ -209,7 +208,8 @@ final class jsonNoExtraFields extends Annotation */ final class jsonExclude extends Annotation -private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) extends CollectionJsonDecoder[A] { +private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A], no_extra: Boolean) + extends CollectionJsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { if (no_extra) { Lexer.char(trace, in, '{') @@ -223,7 +223,7 @@ private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case _: Json.Obj | Json.Null => ctx.rawConstruct(Nil) - case _ => Lexer.error("expected object", trace) + case _ => Lexer.error("expected object", trace) } } @@ -236,17 +236,16 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv .map(true -> _) .getOrElse(false -> identity) - val no_extra = ctx - .annotations - .collectFirst { case _: jsonNoExtraFields => () } - .isDefined || !config.allowExtraFields + val no_extra = ctx.annotations.collectFirst { + case _: jsonNoExtraFields => () + }.isDefined || !config.allowExtraFields if (ctx.params.isEmpty) { new CaseObjectDecoder(ctx, no_extra) } else { new CollectionJsonDecoder[A] { private val (names, aliases): (Array[String], Array[(String, Int)]) = { - val names = Array.ofDim[String](ctx.params.size) + val names = new Array[String](ctx.params.size) val aliasesBuilder = Array.newBuilder[(String, Int)] ctx.params.foreach { var idx = 0 @@ -409,12 +408,11 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv IArray.genericWrapArray(ctx.subtypes.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap: Map[String, Int] = names.zipWithIndex.toMap - def isEnumeration = - (ctx.isEnum && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[?, ?]])) || ( - !ctx.isEnum && ctx.subtypes.forall(_.isObject) - ) + val isEnumeration = config.enumValuesAsStrings && + (ctx.isEnum && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[?, ?]]) || + !ctx.isEnum && ctx.subtypes.forall(_.isObject)) - def discrim = + val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) if (isEnumeration && discrim.isEmpty) { @@ -633,88 +631,124 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv } } - def split[A](ctx: SealedTrait[JsonEncoder, A]): JsonEncoder[A] = { - val isEnumeration = - (ctx.isEnum && ctx.subtypes.forall(_.typeclass == caseObjectEncoder)) || ( - !ctx.isEnum && ctx.subtypes.forall(_.isObject) - ) + def split[A](ctx: SealedTrait[JsonEncoder, A]): JsonEncoder[A] = { + val isEnumeration = config.enumValuesAsStrings && + (ctx.isEnum && ctx.subtypes.forall(_.typeclass == caseObjectEncoder) || + !ctx.isEnum && ctx.subtypes.forall(_.isObject)) val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) + val names: Array[String] = IArray.genericWrapArray(ctx.subtypes.map { p => + p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeInfo.short)) + }).toArray val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) if (isEnumeration && discrim.isEmpty) { new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.choose(a) { sub => - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - JsonEncoder.string.unsafeEncode(name, indent, out) + private val subtypes = ctx.subtypes + + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + var i = 0 + while (i < subtypes.length) { + if (subtypes(i).cast.isDefinedAt(a)) { + JsonEncoder.string.unsafeEncode(names(i), indent, out) + return + } + i += 1 + } } - override final def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - new Right(new Json.Str(name)) + override final def toJsonAST(a: A): Either[String, Json] = { + var i = 0 + while (i < subtypes.length) { + if (subtypes(i).cast.isDefinedAt(a)) { + return new Right(new Json.Str(names(i))) + } + i += 1 + } + throw new IllegalArgumentException // shodn't be reached } } } else if (discrim.isEmpty) { new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.choose(a) { sub => - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - JsonEncoder.string.unsafeEncode(name, indent_, out) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) - JsonEncoder.pad(indent, out) - out.write('}') + private val subtypes = ctx.subtypes + + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + var i = 0 + while (i < subtypes.length) { + val sub = subtypes(i) + if (sub.cast.isDefinedAt(a)) { + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + JsonEncoder.string.unsafeEncode(names(i), indent_, out) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) + JsonEncoder.pad(indent, out) + out.write('}') + return + } + i += 1 + } } - override def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).map { inner => - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - new Json.Obj(Chunk(name -> inner)) + override def toJsonAST(a: A): Either[String, Json] = { + var i = 0 + while (i < subtypes.length) { + val sub = subtypes(i) + if (sub.cast.isDefinedAt(a)) { + return sub.typeclass.toJsonAST(sub.cast(a)).map { inner => + new Json.Obj(Chunk(names(i) -> inner)) + } + } + i += 1 } + throw new IllegalArgumentException // shodn't be reached } } } else { new JsonEncoder[A] { - private val hintField = discrim.get + private val subtypes = ctx.subtypes + private val hintFieldName = discrim.get - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.choose(a) { sub => - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - JsonEncoder.string.unsafeEncode(hintField, indent_, out) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - JsonEncoder.string.unsafeEncode(name, indent_, out) - // whitespace is always off by 2 spaces at the end, probably not worth fixing - val intermediate = new DeriveJsonEncoder.NestedWriter(out, indent_) - sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + var i = 0 + while (i < subtypes.length) { + val sub = subtypes(i) + if (sub.cast.isDefinedAt(a)) { + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + JsonEncoder.string.unsafeEncode(hintFieldName, indent_, out) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + JsonEncoder.string.unsafeEncode(names(i), indent_, out) + // whitespace is always off by 2 spaces at the end, probably not worth fixing + val intermediate = new DeriveJsonEncoder.NestedWriter(out, indent_) + sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) + return + } + i += 1 + } } - override final def toJsonAST(a: A): Either[String, Json] = ctx.choose(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).flatMap { - case o: Json.Obj => - val name = sub.annotations.collectFirst { - case jsonHint(name) => name - }.getOrElse(jsonHintFormat(sub.typeInfo.short)) - new Right(Json.Obj((hintField -> new Json.Str(name)) +: o.fields)) // hint field is always first - case _ => - new Left("expected object") + override final def toJsonAST(a: A): Either[String, Json] = { + var i = 0 + while (i < subtypes.length) { + val sub = subtypes(i) + if (sub.cast.isDefinedAt(a)) { + return sub.typeclass.toJsonAST(sub.cast(a)).flatMap { + case o: Json.Obj => + val hintField = hintFieldName -> new Json.Str(names(i)) + new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first + case _ => + new Left("expected object") + } + } + i += 1 } + throw new IllegalArgumentException // shodn't be reached } } } diff --git a/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala index 155d86754..d4ca8d932 100644 --- a/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-2.13/zio/json/CodecVersionSpecificSpec.scala @@ -7,11 +7,10 @@ import scala.collection.immutable object CodecVersionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("CodecSpec")( + suite("CodecVersionSpecific")( test("ArraySeq") { val jsonStr = """["5XL","2XL","XL"]""" val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) } ) diff --git a/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala index 70fb34146..c9af9f4ba 100644 --- a/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-2.13/zio/json/DecoderVersionSpecificSpec.scala @@ -9,12 +9,11 @@ import scala.collection.immutable object DecoderVersionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("Decoder")( + suite("DecoderVersionSpecific")( suite("fromJson")( test("ArraySeq") { val jsonStr = """["5XL","2XL","XL"]""" val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) } ), @@ -22,7 +21,6 @@ object DecoderVersionSpecificSpec extends ZIOSpecDefault { test("ArraySeq") { val json = Json.Arr(Json.Str("5XL"), Json.Str("2XL"), Json.Str("XL")) val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(json.as[Seq[String]])(isRight(equalTo(expected))) } ) diff --git a/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala b/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala index b8c4ac448..aaabfb137 100644 --- a/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-2.13/zio/json/EncoderVesionSpecificSpec.scala @@ -9,7 +9,7 @@ import scala.collection.immutable object EncoderVesionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("Encoder")( + suite("EncoderVesionSpecific")( suite("toJson")( test("collections") { assert(immutable.ArraySeq[Int]().toJson)(equalTo("[]")) && @@ -22,7 +22,6 @@ object EncoderVesionSpecificSpec extends ZIOSpecDefault { test("collections") { val arrEmpty = Json.Arr() val arr123 = Json.Arr(Json.Num(1), Json.Num(2), Json.Num(3)) - assert(immutable.ArraySeq[Int]().toJsonAST)(isRight(equalTo(arrEmpty))) && assert(immutable.ArraySeq(1, 2, 3).toJsonAST)(isRight(equalTo(arr123))) } diff --git a/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala index 155d86754..d0e7a1091 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala @@ -7,12 +7,37 @@ import scala.collection.immutable object CodecVersionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("CodecSpec")( + suite("CodecVersionSpecific")( test("ArraySeq") { val jsonStr = """["5XL","2XL","XL"]""" val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) + }, + test("Derives for a product type") { + assertZIO(typeCheck { + """ + case class Foo(bar: String) derives JsonCodec + + Foo("bar").toJson.fromJson[Foo] + """ + })(isRight(anything)) + }, + test("Derives for a sum type") { + assertZIO(typeCheck { + """ + enum Foo derives JsonCodec: + case Bar + case Baz(baz: String) + case Qux(foo: Foo) + + (Foo.Qux(Foo.Bar): Foo).toJson.fromJson[Foo] + """ + })(isRight(anything)) + }, + test("Derives and encodes for a union of string-based literals") { + case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonCodec + + assertTrue(Foo("A", Some("A")).toJson.fromJson[Foo] == Right(Foo("A", Some("A")))) } ) } diff --git a/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala index 70fb34146..367cdb97a 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/DecoderVersionSpecificSpec.scala @@ -9,20 +9,125 @@ import scala.collection.immutable object DecoderVersionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("Decoder")( + suite("DecoderVersionSpecific")( suite("fromJson")( test("ArraySeq") { val jsonStr = """["5XL","2XL","XL"]""" val expected = immutable.ArraySeq("5XL", "2XL", "XL") assert(jsonStr.fromJson[immutable.ArraySeq[String]])(isRight(equalTo(expected))) + }, + test("Derives for a product type") { + case class Foo(bar: String) derives JsonDecoder + + assertTrue("{\"bar\": \"hello\"}".fromJson[Foo] == Right(Foo("hello"))) + }, + test("Derives for a sum enum Enumeration type") { + @jsonHintNames(SnakeCase) + enum Foo derives JsonDecoder: + case Bar + case Baz + case Qux + + assertTrue("\"qux\"".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("\"bar\"".fromJson[Foo] == Right(Foo.Bar)) + }, + test("Derives for a sum enum Enumeration type with enumValuesAsStrings = false") { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + enum Foo derives JsonDecoder: + case Bar + case Baz + case Qux + + assertTrue("{\"Qux\":{}}".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("{\"Bar\":{}}".fromJson[Foo] == Right(Foo.Bar)) + }, + test("Derives for a sum sealed trait Enumeration type") { + sealed trait Foo derives JsonDecoder + object Foo: + @jsonHint("Barrr") + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + assertTrue("\"Qux\"".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("\"Barrr\"".fromJson[Foo] == Right(Foo.Bar)) + }, + test("Derives for a sum sealed trait Enumeration type with enumValuesAsStrings = false") { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + sealed trait Foo derives JsonDecoder + object Foo: + @jsonHint("Barrr") + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + assertTrue("{\"Qux\":{}}".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("{\"Barrr\":{}}".fromJson[Foo] == Right(Foo.Bar)) + }, + test("Derives for a sum sealed trait Enumeration type with discriminator") { + @jsonDiscriminator("$type") + sealed trait Foo derives JsonDecoder + object Foo: + @jsonHint("Barrr") + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + assertTrue("""{"$type":"Qux"}""".fromJson[Foo] == Right(Foo.Qux)) && + assertTrue("""{"$type":"Barrr"}""".fromJson[Foo] == Right(Foo.Bar)) + }, + test("skip JSON encoded in a string value") { + @jsonDiscriminator("type") + sealed trait Example derives JsonDecoder { + type Content + def content: Content + } + object Example { + @jsonHint("JSON") + final case class JsonInput(content: String) extends Example { + override type Content = String + } + } + + val json = + """ + |{ + | "content": "\"{\\n \\\"name\\\": \\\"John\\\",\\\"location\\\":\\\"Sydney\\\",\\n \\\"email\\\": \\\"jdoe@test.com\\\"\\n}\"", + | "type": "JSON" + |} + |""".stripMargin.trim + assertTrue(json.fromJson[Example].isRight) + }, + test("Derives for a recursive sum ADT type") { + enum Foo derives JsonDecoder: + case Bar + case Baz(baz: String) + case Qux(foo: Foo) + + assertTrue("{\"Qux\":{\"foo\":{\"Bar\":{}}}}".fromJson[Foo] == Right(Foo.Qux(Foo.Bar))) + }, + test("Derives and decodes for a union of string-based literals") { + case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonDecoder + + assertTrue("""{"aOrB": "A", "optA": "A"}""".fromJson[Foo] == Right(Foo("A", Some("A")))) && + assertTrue("""{"aOrB": "C"}""".fromJson[Foo] == Left(".aOrB(expected one of: A, B)")) + }, + test("Derives and decodes for a custom map key string-based union type") { + case class Foo(aOrB: Map["A" | "B", Int]) derives JsonDecoder + + assertTrue("""{"aOrB": {"A": 1, "B": 2}}""".fromJson[Foo] == Right(Foo(Map("A" -> 1, "B" -> 2)))) && + assertTrue("""{"aOrB": {"C": 1}}""".fromJson[Foo] == Left(".aOrB.C(expected one of: A, B)")) } ), suite("fromJsonAST")( test("ArraySeq") { val json = Json.Arr(Json.Str("5XL"), Json.Str("2XL"), Json.Str("XL")) val expected = immutable.ArraySeq("5XL", "2XL", "XL") - assert(json.as[Seq[String]])(isRight(equalTo(expected))) } ) diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala deleted file mode 100644 index aafb70e4b..000000000 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedCodecSpec.scala +++ /dev/null @@ -1,36 +0,0 @@ -package zio.json - -import zio._ -import zio.test.Assertion._ -import zio.test._ - -object DerivedCodecSpec extends ZIOSpecDefault { - val spec = suite("DerivedCodecSpec")( - test("Derives for a product type") { - assertZIO(typeCheck { - """ - case class Foo(bar: String) derives JsonCodec - - Foo("bar").toJson.fromJson[Foo] - """ - })(isRight(anything)) - }, - test("Derives for a sum type") { - assertZIO(typeCheck { - """ - enum Foo derives JsonCodec: - case Bar - case Baz(baz: String) - case Qux(foo: Foo) - - (Foo.Qux(Foo.Bar): Foo).toJson.fromJson[Foo] - """ - })(isRight(anything)) - }, - test("Derives and encodes for a union of string-based literals") { - case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonCodec - - assertTrue(Foo("A", Some("A")).toJson.fromJson[Foo] == Right(Foo("A", Some("A")))) - } - ) -} diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala deleted file mode 100644 index 0007bf618..000000000 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedDecoderSpec.scala +++ /dev/null @@ -1,91 +0,0 @@ -package zio.json - -import zio._ -import zio.test.Assertion._ -import zio.test._ - -object DerivedDecoderSpec extends ZIOSpecDefault { - - val spec = suite("DerivedDecoderSpec")( - test("Derives for a product type") { - case class Foo(bar: String) derives JsonDecoder - - assertTrue("{\"bar\": \"hello\"}".fromJson[Foo] == Right(Foo("hello"))) - }, - test("Derives for a sum enum Enumeration type") { - @jsonHintNames(SnakeCase) - enum Foo derives JsonDecoder: - case Bar - case Baz - case Qux - - assertTrue("\"qux\"".fromJson[Foo] == Right(Foo.Qux)) - assertTrue("\"bar\"".fromJson[Foo] == Right(Foo.Bar)) - }, - test("Derives for a sum sealed trait Enumeration type") { - sealed trait Foo derives JsonDecoder - object Foo: - @jsonHint("Barrr") - case object Bar extends Foo - case object Baz extends Foo - case object Qux extends Foo - - assertTrue("\"Qux\"".fromJson[Foo] == Right(Foo.Qux)) - assertTrue("\"Barrr\"".fromJson[Foo] == Right(Foo.Bar)) - }, - test("Derives for a sum sealed trait Enumeration type with discriminator") { - @jsonDiscriminator("$type") - sealed trait Foo derives JsonDecoder - object Foo: - @jsonHint("Barrr") - case object Bar extends Foo - case object Baz extends Foo - case object Qux extends Foo - - assertTrue("""{"$type":"Qux"}""".fromJson[Foo] == Right(Foo.Qux)) - assertTrue("""{"$type":"Barrr"}""".fromJson[Foo] == Right(Foo.Bar)) - }, - test("skip JSON encoded in a string value") { - @jsonDiscriminator("type") - sealed trait Example derives JsonDecoder { - type Content - def content: Content - } - object Example { - @jsonHint("JSON") - final case class JsonInput(content: String) extends Example { - override type Content = String - } - } - - val json = - """ - |{ - | "content": "\"{\\n \\\"name\\\": \\\"John\\\",\\\"location\\\":\\\"Sydney\\\",\\n \\\"email\\\": \\\"jdoe@test.com\\\"\\n}\"", - | "type": "JSON" - |} - |""".stripMargin.trim - assertTrue(json.fromJson[Example].isRight) - }, - test("Derives for a recursive sum ADT type") { - enum Foo derives JsonDecoder: - case Bar - case Baz(baz: String) - case Qux(foo: Foo) - - assertTrue("{\"Qux\":{\"foo\":{\"Bar\":{}}}}".fromJson[Foo] == Right(Foo.Qux(Foo.Bar))) - }, - test("Derives and decodes for a union of string-based literals") { - case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonDecoder - - assertTrue("""{"aOrB": "A", "optA": "A"}""".fromJson[Foo] == Right(Foo("A", Some("A")))) && - assertTrue("""{"aOrB": "C"}""".fromJson[Foo] == Left(".aOrB(expected one of: A, B)")) - }, - test("Derives and decodes for a custom map key string-based union type") { - case class Foo(aOrB: Map["A" | "B", Int]) derives JsonDecoder - - assertTrue("""{"aOrB": {"A": 1, "B": 2}}""".fromJson[Foo] == Right(Foo(Map("A" -> 1, "B" -> 2)))) && - assertTrue("""{"aOrB": {"C": 1}}""".fromJson[Foo] == Left(".aOrB.C(expected one of: A, B)")) - } - ) -} diff --git a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala deleted file mode 100644 index 05430a2c9..000000000 --- a/zio-json/shared/src/test/scala-3/zio/json/DerivedEncoderSpec.scala +++ /dev/null @@ -1,69 +0,0 @@ -package zio.json - -import zio._ -import zio.test.Assertion._ -import zio.test._ - -object DerivedEncoderSpec extends ZIOSpecDefault { - val spec = suite("DerivedEncoderSpec")( - test("Derives for a product type") { - case class Foo(bar: String) derives JsonEncoder - - val json = Foo("bar").toJson - - assertTrue(json == """{"bar":"bar"}""") - }, - test("Derives for a sum enum Enumeration type") { - enum Foo derives JsonEncoder: - case Bar - case Baz - case Qux - - val json = (Foo.Qux: Foo).toJson - - assertTrue(json == """"Qux"""") - }, - test("Derives for a sum enum Enumeration type with discriminator") { - @jsonDiscriminator("$type") - enum Foo derives JsonEncoder: - case Bar - case Baz - case Qux - - val json = (Foo.Qux: Foo).toJson - - assertTrue(json == """{"$type":"Qux"}""") - }, - test("Derives for a sum sealed trait Enumeration type") { - sealed trait Foo derives JsonEncoder - object Foo: - case object Bar extends Foo - case object Baz extends Foo - case object Qux extends Foo - - val json = (Foo.Qux: Foo).toJson - - assertTrue(json == """"Qux"""") - }, - test("Derives for a sum ADT type") { - enum Foo derives JsonEncoder: - case Bar - case Baz(baz: String) - case Qux(foo: Foo) - - val json = (Foo.Qux(Foo.Bar): Foo).toJson - - assertTrue(json == """{"Qux":{"foo":{"Bar":{}}}}""") - }, - test("Derives and encodes for a union of string-based literals") { - case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonEncoder - - assertTrue(Foo("A", Some("A")).toJson == """{"aOrB":"A","optA":"A"}""") - }, - test("Derives and encodes for a custom map key string-based union type") { - case class Foo(aOrB: Map["A" | "B", Int]) derives JsonEncoder - - assertTrue(Foo(Map("A" -> 1, "B" -> 2)).toJson == """{"aOrB":{"A":1,"B":2}}""") - } - ) -} diff --git a/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala index b8c4ac448..2cad2a16c 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala @@ -9,13 +9,79 @@ import scala.collection.immutable object EncoderVesionSpecificSpec extends ZIOSpecDefault { val spec: Spec[Environment, Any] = - suite("Encoder")( + suite("EncoderVesionSpecific")( suite("toJson")( test("collections") { assert(immutable.ArraySeq[Int]().toJson)(equalTo("[]")) && assert(immutable.ArraySeq(1, 2, 3).toJson)(equalTo("[1,2,3]")) && assert(immutable.ArraySeq[String]().toJsonPretty)(equalTo("[]")) && assert(immutable.ArraySeq("foo", "bar").toJsonPretty)(equalTo("[\n \"foo\",\n \"bar\"\n]")) + }, + test("Derives for a product type") { + case class Foo(bar: String) derives JsonEncoder + + val json = Foo("bar").toJson + assertTrue(json == """{"bar":"bar"}""") + }, + test("Derives for a sum enum Enumeration type") { + enum Foo derives JsonEncoder: + case Bar + case Baz + case Qux + + val json = (Foo.Qux: Foo).toJson + assertTrue(json == """"Qux"""") + }, + test("Derives for a sum enum Enumeration type with enumValuesAsStrings = false") { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + enum Foo derives JsonEncoder: + case Bar + case Baz + case Qux + + val json = (Foo.Qux: Foo).toJson + assertTrue(json == """{"Qux":{}}""") + }, + test("Derives for a sum enum Enumeration type with discriminator") { + @jsonDiscriminator("$type") + enum Foo derives JsonEncoder: + case Bar + case Baz + case Qux + + val json = (Foo.Qux: Foo).toJson + assertTrue(json == """{"$type":"Qux"}""") + }, + test("Derives for a sum sealed trait Enumeration type") { + sealed trait Foo derives JsonEncoder + object Foo: + case object Bar extends Foo + case object Baz extends Foo + case object Qux extends Foo + + val json = (Foo.Qux: Foo).toJson + assertTrue(json == """"Qux"""") + }, + test("Derives for a sum ADT type") { + enum Foo derives JsonEncoder: + case Bar + case Baz(baz: String) + case Qux(foo: Foo) + + val json = (Foo.Qux(Foo.Bar): Foo).toJson + assertTrue(json == """{"Qux":{"foo":{"Bar":{}}}}""") + }, + test("Derives and encodes for a union of string-based literals") { + case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonEncoder + + assertTrue(Foo("A", Some("A")).toJson == """{"aOrB":"A","optA":"A"}""") + }, + test("Derives and encodes for a custom map key string-based union type") { + case class Foo(aOrB: Map["A" | "B", Int]) derives JsonEncoder + + assertTrue(Foo(Map("A" -> 1, "B" -> 2)).toJson == """{"aOrB":{"A":1,"B":2}}""") } ), suite("toJsonAST")( diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index d22a683a1..56dcd4aa3 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -350,6 +350,18 @@ object DecoderSpec extends ZIOSpecDefault { assert("""{"Child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2()))) && assert("""{"type":"Child1"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) }, + test("sum encoding with enumValuesAsStrings = true") { + import examplesumobjects1._ + + assert(""""Child1"""".fromJson[Parent])(isRight(equalTo(Child1))) && + assert(""""Child2"""".fromJson[Parent])(isRight(equalTo(Child2))) + }, + test("sum encoding with enumValuesAsStrings = false") { + import examplesumobjects2._ + + assert("""{"Child1":{}}""".fromJson[Parent])(isRight(equalTo(Child1))) && + assert("""{"Child2":{}}""".fromJson[Parent])(isRight(equalTo(Child2))) + }, test("sum encoding with hint names") { import examplesumhintnames._ @@ -896,6 +908,38 @@ object DecoderSpec extends ZIOSpecDefault { } + object examplesumobjects1 { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = true) + + sealed abstract class Parent + + object Parent { + implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] + } + + case object Child1 extends Parent + + case object Child2 extends Parent + + } + + object examplesumobjects2 { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + sealed abstract class Parent + + object Parent { + implicit val decoder: JsonDecoder[Parent] = DeriveJsonDecoder.gen[Parent] + } + + case object Child1 extends Parent + + case object Child2 extends Parent + + } + object examplesumhintnames { @jsonHintNames(CamelCase) diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index c210f703c..39985aa7a 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -376,6 +376,18 @@ object EncoderSpec extends ZIOSpecDefault { assert((Child1(): Parent).toJsonPretty)(equalTo("{\n \"Child1\" : {}\n}")) && assert((Child2(): Parent).toJsonPretty)(equalTo("{\n \"Cain\" : {}\n}")) }, + test("sum encoding with enumValuesAsStrings = true") { + import examplesumobjects1._ + + assert((Child1: Parent).toJson)(equalTo(""""Child1"""")) && + assert((Child2: Parent).toJson)(equalTo(""""Cain"""")) + }, + test("sum encoding with enumValuesAsStrings = false") { + import examplesumobjects2._ + + assert((Child1: Parent).toJson)(equalTo("""{"Child1":{}}""")) && + assert((Child2: Parent).toJson)(equalTo("""{"Cain":{}}""")) + }, test("sum alternative encoding") { import examplealtsum._ @@ -588,6 +600,40 @@ object EncoderSpec extends ZIOSpecDefault { } + object examplesumobjects1 { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = true) + + sealed abstract class Parent + + object Parent { + implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] + } + + case object Child1 extends Parent + + @jsonHint("Cain") + case object Child2 extends Parent + + } + + object examplesumobjects2 { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = false) + + sealed abstract class Parent + + object Parent { + implicit val encoder: JsonEncoder[Parent] = DeriveJsonEncoder.gen[Parent] + } + + case object Child1 extends Parent + + @jsonHint("Cain") + case object Child2 extends Parent + + } + object examplesumhintnames { @jsonHintNames(CamelCase) From f2921a0517572cc1993bb4f5e53e0e8162574d5e Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 25 Feb 2025 10:50:42 +0100 Subject: [PATCH 189/311] More efficient encoding of sum types (#1340) --- build.sbt | 7 +- .../src/main/scala-2.x/zio/json/macros.scala | 150 ++++++++++++------ .../src/main/scala-3/zio/json/macros.scala | 119 +++++++------- 3 files changed, 172 insertions(+), 104 deletions(-) diff --git a/build.sbt b/build.sbt index 395ed9d54..7da8d25d8 100644 --- a/build.sbt +++ b/build.sbt @@ -216,9 +216,12 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) ) Seq(file) }.taskValue, - inConfig(Jmh)(org.scalafmt.sbt.ScalafmtPlugin.scalafmtConfigSettings) + inConfig(Jmh)(org.scalafmt.sbt.ScalafmtPlugin.scalafmtConfigSettings), + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), + mimaBinaryIssueFilters ++= Seq( + exclude[Problem]("zio.json.CaseObjectDecoder.*") // FIXME: false negative reported by mima + ) ) - .settings(testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")) .jsSettings( mimaBinaryIssueFilters ++= Seq( exclude[Problem]("zio.JsonPackagePlatformSpecific.*"), diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index fb29986e5..6b6d73562 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -4,7 +4,7 @@ import magnolia1._ import zio.Chunk import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.internal.{ FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } +import zio.json.internal.{ FastStringWrite, FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } import scala.annotation._ import scala.language.experimental.macros @@ -403,14 +403,11 @@ object DeriveJsonDecoder { val matrix = new StringMatrix(names) lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap = names.zipWithIndex.toMap - - val isEnumeration = config.enumValuesAsStrings && - ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[JsonDecoder, _]]) - val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - - if (isEnumeration && discrim.isEmpty) { + lazy val isEnumeration = config.enumValuesAsStrings && + ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[JsonDecoder, _]]) + if (discrim.isEmpty && isEnumeration) { new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val idx = Lexer.enumeration(trace, in, matrix) @@ -608,70 +605,131 @@ object DeriveJsonEncoder { } def split[A](ctx: SealedTrait[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = { - val isEnumeration = config.enumValuesAsStrings && - ctx.subtypes.forall(_.typeclass == caseObjectEncoder) val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val names: Array[String] = ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeName.short)) }.toArray + val encodedNames: Array[String] = names.map { name => + val out = new FastStringWrite(64) + JsonEncoder.string.unsafeEncode(name, None, out) + out.toString + } + lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonEncoder[Any]]] val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - - if (isEnumeration && discrim.isEmpty) { + lazy val isEnumeration = config.enumValuesAsStrings && + ctx.subtypes.forall(_.typeclass == caseObjectEncoder) + if (discrim.isEmpty && isEnumeration) { new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.split(a) { sub => - JsonEncoder.string.unsafeEncode(names(sub.index), indent, out) + private[this] val casts = ctx.subtypes.map(_.cast).toArray + + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + var idx = 0 + while (idx < casts.length) { + if (casts(idx).isDefinedAt(a)) { + out.write(encodedNames(idx)) + return + } + idx += 1 + } } - override final def toJsonAST(a: A): Either[String, Json] = ctx.split(a) { sub => - new Right(new Json.Str(names(sub.index))) + override final def toJsonAST(a: A): Either[String, Json] = { + var idx = 0 + while (idx < casts.length) { + if (casts(idx).isDefinedAt(a)) { + return new Right(new Json.Str(names(idx))) + } + idx += 1 + } + throw new IllegalArgumentException // shodn't be reached } } } else if (discrim.isEmpty) { new JsonEncoder[A] { - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.split(a) { sub => - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - JsonEncoder.string.unsafeEncode(names(sub.index), indent_, out) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) - JsonEncoder.pad(indent, out) - out.write('}') + private[this] val casts = ctx.subtypes.map(_.cast).toArray + + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + var idx = 0 + while (idx < casts.length) { + val cast = casts(idx) + if (cast.isDefinedAt(a)) { + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + out.write(encodedNames(idx)) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + tcs(idx).unsafeEncode(cast(a), indent_, out) + JsonEncoder.pad(indent, out) + out.write('}') + return + } + idx += 1 + } } - override def toJsonAST(a: A): Either[String, Json] = ctx.split(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).map { inner => - Json.Obj(Chunk(names(sub.index) -> inner)) + override def toJsonAST(a: A): Either[String, Json] = { + var idx = 0 + while (idx < casts.length) { + val cast = casts(idx) + if (cast.isDefinedAt(a)) { + return tcs(idx).toJsonAST(cast(a)).map { inner => + new Json.Obj(Chunk(names(idx) -> inner)) + } + } + idx += 1 } + throw new IllegalArgumentException // shodn't be reached } } } else { new JsonEncoder[A] { - private[this] val hintfield = discrim.get + private[this] val casts = ctx.subtypes.map(_.cast).toArray + private[this] val hintFieldName = discrim.get + private[this] val encodedHintFieldName = { + val out = new FastStringWrite(64) + JsonEncoder.string.unsafeEncode(hintFieldName, None, out) + out.toString + } - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = ctx.split(a) { sub => - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - JsonEncoder.string.unsafeEncode(hintfield, indent_, out) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - JsonEncoder.string.unsafeEncode(names(sub.index), indent_, out) - // whitespace is always off by 2 spaces at the end, probably not worth fixing - val intermediate = new NestedWriter(out, indent_) - sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) + def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { + var idx = 0 + while (idx < casts.length) { + val cast = casts(idx) + if (cast.isDefinedAt(a)) { + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + out.write(encodedHintFieldName) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + out.write(encodedNames(idx)) + // whitespace is always off by 2 spaces at the end, probably not worth fixing + tcs(idx).unsafeEncode(cast(a), indent, new NestedWriter(out, indent_)) + return + } + idx += 1 + } } - override def toJsonAST(a: A): Either[String, Json] = ctx.split(a) { sub => - sub.typeclass.toJsonAST(sub.cast(a)).flatMap { - case o: Json.Obj => - new Right(Json.Obj((hintfield -> Json.Str(names(sub.index))) +: o.fields)) // hint field is always first - case _ => - new Left("expected object") + override final def toJsonAST(a: A): Either[String, Json] = { + var idx = 0 + while (idx < casts.length) { + val cast = casts(idx) + if (cast.isDefinedAt(a)) { + return tcs(idx).toJsonAST(cast(a)).flatMap { + case o: Json.Obj => + val hintField = hintFieldName -> new Json.Str(names(idx)) + new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first + case _ => + new Left("expected object") + } + } + idx += 1 } + throw new IllegalArgumentException // shodn't be reached } } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index db41270cf..b82f1e912 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -8,7 +8,7 @@ import scala.reflect.* import zio.Chunk import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.internal.{ FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } +import zio.json.internal.{ FastStringWrite, FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } import scala.annotation._ import scala.collection.Factory @@ -407,15 +407,12 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv lazy val tcs: Array[JsonDecoder[Any]] = IArray.genericWrapArray(ctx.subtypes.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap: Map[String, Int] = names.zipWithIndex.toMap - - val isEnumeration = config.enumValuesAsStrings && - (ctx.isEnum && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[?, ?]]) || - !ctx.isEnum && ctx.subtypes.forall(_.isObject)) - val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - - if (isEnumeration && discrim.isEmpty) { + lazy val isEnumeration = config.enumValuesAsStrings && + (ctx.isEnum && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[?, ?]]) || + !ctx.isEnum && ctx.subtypes.forall(_.isObject)) + if (discrim.isEmpty && isEnumeration) { new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val idx = Lexer.enumeration(trace, in, matrix) @@ -632,121 +629,131 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv } def split[A](ctx: SealedTrait[JsonEncoder, A]): JsonEncoder[A] = { - val isEnumeration = config.enumValuesAsStrings && - (ctx.isEnum && ctx.subtypes.forall(_.typeclass == caseObjectEncoder) || - !ctx.isEnum && ctx.subtypes.forall(_.isObject)) val jsonHintFormat: JsonMemberFormat = ctx.annotations.collectFirst { case jsonHintNames(format) => format }.getOrElse(config.sumTypeMapping) val names: Array[String] = IArray.genericWrapArray(ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeInfo.short)) }).toArray + val encodedNames: Array[String] = names.map { name => + val out = new FastStringWrite(64) + JsonEncoder.string.unsafeEncode(name, None, out) + out.toString + } + lazy val tcs = + IArray.genericWrapArray(ctx.subtypes.map(_.typeclass)).toArray.asInstanceOf[Array[JsonEncoder[Any]]] val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) - - if (isEnumeration && discrim.isEmpty) { + lazy val isEnumeration = config.enumValuesAsStrings && + (ctx.isEnum && ctx.subtypes.forall(_.typeclass == caseObjectEncoder) || + !ctx.isEnum && ctx.subtypes.forall(_.isObject)) + if (discrim.isEmpty && isEnumeration) { new JsonEncoder[A] { - private val subtypes = ctx.subtypes + private val casts = IArray.genericWrapArray(ctx.subtypes.map(_.cast)).toArray def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { - var i = 0 - while (i < subtypes.length) { - if (subtypes(i).cast.isDefinedAt(a)) { - JsonEncoder.string.unsafeEncode(names(i), indent, out) + var idx = 0 + while (idx < casts.length) { + if (casts(idx).isDefinedAt(a)) { + out.write(encodedNames(idx)) return } - i += 1 + idx += 1 } } override final def toJsonAST(a: A): Either[String, Json] = { - var i = 0 - while (i < subtypes.length) { - if (subtypes(i).cast.isDefinedAt(a)) { - return new Right(new Json.Str(names(i))) + var idx = 0 + while (idx < casts.length) { + if (casts(idx).isDefinedAt(a)) { + return new Right(new Json.Str(names(idx))) } - i += 1 + idx += 1 } throw new IllegalArgumentException // shodn't be reached } } } else if (discrim.isEmpty) { new JsonEncoder[A] { - private val subtypes = ctx.subtypes + private val casts = IArray.genericWrapArray(ctx.subtypes.map(_.cast)).toArray def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { - var i = 0 - while (i < subtypes.length) { - val sub = subtypes(i) - if (sub.cast.isDefinedAt(a)) { + var idx = 0 + while (idx < casts.length) { + val cast = casts(idx) + if (cast.isDefinedAt(a)) { out.write('{') val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) - JsonEncoder.string.unsafeEncode(names(i), indent_, out) + out.write(encodedNames(idx)) if (indent.isEmpty) out.write(':') else out.write(" : ") - sub.typeclass.unsafeEncode(sub.cast(a), indent_, out) + tcs(idx).unsafeEncode(cast(a), indent_, out) JsonEncoder.pad(indent, out) out.write('}') return } - i += 1 + idx += 1 } } override def toJsonAST(a: A): Either[String, Json] = { - var i = 0 - while (i < subtypes.length) { - val sub = subtypes(i) - if (sub.cast.isDefinedAt(a)) { - return sub.typeclass.toJsonAST(sub.cast(a)).map { inner => - new Json.Obj(Chunk(names(i) -> inner)) + var idx = 0 + while (idx < casts.length) { + val cast = casts(idx) + if (cast.isDefinedAt(a)) { + return tcs(idx).toJsonAST(cast(a)).map { inner => + new Json.Obj(Chunk(names(idx) -> inner)) } } - i += 1 + idx += 1 } throw new IllegalArgumentException // shodn't be reached } } } else { new JsonEncoder[A] { - private val subtypes = ctx.subtypes + private val casts = IArray.genericWrapArray(ctx.subtypes.map(_.cast)).toArray private val hintFieldName = discrim.get + private val encodedHintFieldName = { + val out = new FastStringWrite(64) + JsonEncoder.string.unsafeEncode(hintFieldName, None, out) + out.toString + } def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { - var i = 0 - while (i < subtypes.length) { - val sub = subtypes(i) - if (sub.cast.isDefinedAt(a)) { + var idx = 0 + while (idx < casts.length) { + val cast = casts(idx) + if (cast.isDefinedAt(a)) { out.write('{') val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) - JsonEncoder.string.unsafeEncode(hintFieldName, indent_, out) + out.write(encodedHintFieldName) if (indent.isEmpty) out.write(':') else out.write(" : ") - JsonEncoder.string.unsafeEncode(names(i), indent_, out) + out.write(encodedNames(idx)) // whitespace is always off by 2 spaces at the end, probably not worth fixing - val intermediate = new DeriveJsonEncoder.NestedWriter(out, indent_) - sub.typeclass.unsafeEncode(sub.cast(a), indent, intermediate) + tcs(idx).unsafeEncode(cast(a), indent, new DeriveJsonEncoder.NestedWriter(out, indent_)) return } - i += 1 + idx += 1 } } override final def toJsonAST(a: A): Either[String, Json] = { - var i = 0 - while (i < subtypes.length) { - val sub = subtypes(i) - if (sub.cast.isDefinedAt(a)) { - return sub.typeclass.toJsonAST(sub.cast(a)).flatMap { + var idx = 0 + while (idx < casts.length) { + val cast = casts(idx) + if (cast.isDefinedAt(a)) { + return tcs(idx).toJsonAST(cast(a)).flatMap { case o: Json.Obj => - val hintField = hintFieldName -> new Json.Str(names(i)) + val hintField = hintFieldName -> new Json.Str(names(idx)) new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first case _ => new Left("expected object") } } - i += 1 + idx += 1 } throw new IllegalArgumentException // shodn't be reached } From 75d064c2e83c0eb4e0edf3e0392bfaf1a329d8a0 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 25 Feb 2025 21:45:52 +0100 Subject: [PATCH 190/311] More efficient encoding of product types (#1341) --- .../src/main/scala-2.x/zio/json/macros.scala | 294 +++++++----------- .../src/main/scala-3/zio/json/macros.scala | 274 ++++++---------- .../src/main/scala/zio/json/JsonDecoder.scala | 164 +++++----- .../src/main/scala/zio/json/JsonEncoder.scala | 4 +- .../zio/json/internal/FieldEncoder.scala | 19 +- .../src/main/scala/zio/json/package.scala | 8 +- 6 files changed, 314 insertions(+), 449 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 6b6d73562..c54f81f8f 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -4,7 +4,7 @@ import magnolia1._ import zio.Chunk import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.internal.{ FastStringWrite, FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } +import zio.json.internal.{ FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } import scala.annotation._ import scala.language.experimental.macros @@ -219,61 +219,50 @@ object DeriveJsonDecoder { type Typeclass[A] = JsonDecoder[A] def join[A](ctx: CaseClass[JsonDecoder, A])(implicit config: JsonCodecConfiguration): JsonDecoder[A] = { - val (transformNames, nameTransform): (Boolean, String => String) = - ctx.annotations.collectFirst { case jsonMemberNames(format) => format } - .orElse(Some(config.fieldNameMapping)) - .filter(_ != IdentityFormat) - .map(true -> _) - .getOrElse(false -> identity _) - + val nameTransform = + ctx.annotations.collectFirst { case jsonMemberNames(format) => format }.getOrElse(config.fieldNameMapping) val no_extra = ctx.annotations.collectFirst { case _: jsonNoExtraFields => () }.isDefined || !config.allowExtraFields - - if (ctx.parameters.isEmpty) - new CaseObjectDecoder(ctx, no_extra) - else - new CollectionJsonDecoder[A] { - private[this] val (names, aliases): (Array[String], Array[(String, Int)]) = { - val names = new Array[String](ctx.parameters.size) - val aliasesBuilder = Array.newBuilder[(String, Int)] - ctx.parameters.foreach { - var idx = 0 - p => - names(idx) = p.annotations.collectFirst { case jsonField(name) => name } - .getOrElse(if (transformNames) nameTransform(p.label) else p.label) - aliasesBuilder ++= p.annotations.flatMap { - case jsonAliases(alias, aliases @ _*) => (alias +: aliases).map(_ -> idx) - case _ => Seq.empty - } - idx += 1 - } - val aliases = aliasesBuilder.result() - val allFieldNames = names ++ aliases.map(_._1) - if (allFieldNames.length != allFieldNames.distinct.length) { - val aliasNames = aliases.map(_._1) - val collisions = aliasNames - .filter(alias => names.contains(alias) || aliases.count(a => a._1 == alias) > 1) - .distinct - val msg = s"Field names and aliases in case class ${ctx.typeName.full} must be distinct, " + - s"alias(es) ${collisions.mkString(",")} collide with a field or another alias" - throw new AssertionError(msg) - } - (names, aliases) + if (ctx.parameters.isEmpty) new CaseObjectDecoder(ctx, no_extra) + else { + val (names, aliases): (Array[String], Array[(String, Int)]) = { + val names = new Array[String](ctx.parameters.size) + val aliasesBuilder = Array.newBuilder[(String, Int)] + ctx.parameters.foreach { + var idx = 0 + p => + names(idx) = p.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(p.label)) + aliasesBuilder ++= p.annotations.flatMap { + case jsonAliases(alias, aliases @ _*) => (alias +: aliases).map(_ -> idx) + case _ => Seq.empty + } + idx += 1 + } + val aliases = aliasesBuilder.result() + val allFieldNames = names ++ aliases.map(_._1) + if (allFieldNames.length != allFieldNames.distinct.length) { + val aliasNames = aliases.map(_._1) + val collisions = aliasNames + .filter(alias => names.contains(alias) || aliases.count(a => a._1 == alias) > 1) + .distinct + val msg = s"Field names and aliases in case class ${ctx.typeName.full} must be distinct, " + + s"alias(es) ${collisions.mkString(",")} collide with a field or another alias" + throw new AssertionError(msg) } - private[this] val len = names.length - private[this] val matrix = new StringMatrix(names, aliases) - private[this] val spans = names.map(JsonError.ObjectAccess) - private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray - private[this] lazy val tcs = - ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] + (names, aliases) + } + new CollectionJsonDecoder[A] { + private[this] val len = names.length + private[this] val matrix = new StringMatrix(names, aliases) + private[this] val spans = names.map(JsonError.ObjectAccess) + private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray + private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap - private[this] val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.decoding }.getOrElse(config.explicitEmptyCollections.decoding) - private[this] val missingValueDecoder = if (explicitEmptyCollections) { lazy val missingValueDecoders = tcs.map { d => @@ -317,31 +306,33 @@ object DeriveJsonDecoder { Lexer.char(trace, in, '{') // TODO it would be more efficient to have a solution that didn't box - // primitives, but Magnolia does not expose an API for that. Adding + // primitives, but Magnolia does not ealiasesxpose an API for that. Adding // such a feature to Magnolia is the only way to avoid this, e.g. a // ctx.createMutableCons that specialises on the types (with some way // of noting that things have been initialised), which can be called // to instantiate the case class. Would also require JsonDecoder to be // specialised. val ps = new Array[Any](len) - if (Lexer.firstField(trace, in)) + if (Lexer.firstField(trace, in)) { do { val idx = Lexer.field(trace, in, matrix) - if (idx != -1) { - if (ps(idx) != null) Lexer.error("duplicate", trace) - val default = defaults(idx) - ps(idx) = - if ( - (default eq null) || in.nextNonWhitespace() != 'n' && { - in.retract() - true - } - ) tcs(idx).unsafeDecode(spans(idx) :: trace, in) - else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() - else Lexer.error("expected 'null'", spans(idx) :: trace) + if (idx >= 0) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if ( + (default eq null) || in.nextNonWhitespace() != 'n' && { + in.retract() + true + } + ) tcs(idx).unsafeDecode(spans(idx) :: trace, in) + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() + else Lexer.error("expected 'null'", spans(idx) :: trace) + } else Lexer.error("duplicate", trace) } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) } while (Lexer.nextField(trace, in)) + } var idx = 0 while (idx < len) { if (ps(idx) == null) { @@ -385,6 +376,7 @@ object DeriveJsonDecoder { case _ => Lexer.error("expected object", trace) } } + } } def split[A](ctx: SealedTrait[JsonDecoder, A])(implicit config: JsonCodecConfiguration): JsonDecoder[A] = { @@ -411,7 +403,7 @@ object DeriveJsonDecoder { new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val idx = Lexer.enumeration(trace, in, matrix) - if (idx != -1) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + if (idx >= 0) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) else Lexer.error("invalid enumeration value", trace) } @@ -434,7 +426,7 @@ object DeriveJsonDecoder { Lexer.char(trace, in, '{') if (Lexer.firstField(trace, in)) { val idx = Lexer.field(trace, in, matrix) - if (idx != -1) { + if (idx >= 0) { val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] Lexer.char(trace, in, '}') a @@ -464,9 +456,9 @@ object DeriveJsonDecoder { Lexer.char(trace, in_, '{') if (Lexer.firstField(trace, in_)) { do { - if (Lexer.field(trace, in_, hintmatrix) != -1) { + if (Lexer.field(trace, in_, hintmatrix) >= 0) { val idx = Lexer.enumeration(trace, in_, matrix) - if (idx != -1) { + if (idx >= 0) { in_.rewind() return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] } else Lexer.error("invalid disambiguator", trace) @@ -511,31 +503,17 @@ object DeriveJsonEncoder { type Typeclass[A] = JsonEncoder[A] def join[A](ctx: CaseClass[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = - if (ctx.parameters.isEmpty) - caseObjectEncoder.narrow[A] - else + if (ctx.parameters.isEmpty) caseObjectEncoder.narrow[A] + else { + val nameTransform = + ctx.annotations.collectFirst { case jsonMemberNames(format) => format }.getOrElse(config.fieldNameMapping) + val params = ctx.parameters.filter(p => p.annotations.collectFirst { case _: jsonExclude => () }.isEmpty).toArray + val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.encoding } + .getOrElse(config.explicitEmptyCollections.encoding) new JsonEncoder[A] { - private[this] val (transformNames, nameTransform): (Boolean, String => String) = - ctx.annotations.collectFirst { case jsonMemberNames(format) => format } - .orElse(Some(config.fieldNameMapping)) - .filter(_ != IdentityFormat) - .map(true -> _) - .getOrElse(false -> identity) - private[this] val params = ctx.parameters - .filter(p => p.annotations.collectFirst { case _: jsonExclude => () }.isEmpty) - .toArray - - private[this] val explicitNulls = - config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - private[this] val explicitEmptyCollections = - ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => - a.encoding - }.getOrElse(config.explicitEmptyCollections.encoding) - private[this] lazy val fields: Array[FieldEncoder[Any, Param[JsonEncoder, A]]] = params.map { p => - val name = p.annotations.collectFirst { case jsonField(name) => - name - }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) + val name = p.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(p.label)) val withExplicitNulls = explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) val withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.encoding @@ -562,27 +540,28 @@ object DeriveJsonEncoder { var idx = 0 var prevFields = false // whether any fields have been written while (idx < fields.length) { - val field = fields(idx) - val p = field.p.dereference(a) + val field = fields(idx) + idx += 1 val encoder = field.encoder + val p = field.p.dereference(a) if ({ (field.flags: @switch) match { - case 0 => !encoder.isEmpty(p) && !encoder.isNothing(p) - case 1 => !encoder.isNothing(p) - case 2 => !encoder.isEmpty(p) - case _ => true + case 0 => encoder.isEmpty(p) || encoder.isNothing(p) + case 1 => encoder.isNothing(p) + case 2 => encoder.isEmpty(p) + case _ => false } - }) { + }) () + else { if (prevFields) { out.write(',') JsonEncoder.pad(indent_, out) } else prevFields = true - JsonEncoder.string.unsafeEncode(field.name, indent_, out) + out.write(field.encodedName) if (indent.isEmpty) out.write(':') else out.write(" : ") encoder.unsafeEncode(p, indent_, out) } - idx += 1 } JsonEncoder.pad(indent, out) out.write('}') @@ -603,6 +582,7 @@ object DeriveJsonEncoder { } .map(Json.Obj.apply) } + } def split[A](ctx: SealedTrait[JsonEncoder, A])(implicit config: JsonCodecConfiguration): JsonEncoder[A] = { val jsonHintFormat: JsonMemberFormat = @@ -610,12 +590,8 @@ object DeriveJsonEncoder { val names: Array[String] = ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeName.short)) }.toArray - val encodedNames: Array[String] = names.map { name => - val out = new FastStringWrite(64) - JsonEncoder.string.unsafeEncode(name, None, out) - out.toString - } - lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonEncoder[Any]]] + val encodedNames: Array[String] = names.map(name => JsonEncoder.string.encodeJson(name, None).toString) + lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonEncoder[Any]]] val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) lazy val isEnumeration = config.enumValuesAsStrings && @@ -626,24 +602,14 @@ object DeriveJsonEncoder { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { var idx = 0 - while (idx < casts.length) { - if (casts(idx).isDefinedAt(a)) { - out.write(encodedNames(idx)) - return - } - idx += 1 - } + while (!casts(idx).isDefinedAt(a)) idx += 1 + out.write(encodedNames(idx)) } override final def toJsonAST(a: A): Either[String, Json] = { var idx = 0 - while (idx < casts.length) { - if (casts(idx).isDefinedAt(a)) { - return new Right(new Json.Str(names(idx))) - } - idx += 1 - } - throw new IllegalArgumentException // shodn't be reached + while (!casts(idx).isDefinedAt(a)) idx += 1 + new Right(new Json.Str(names(idx))) } } } else if (discrim.isEmpty) { @@ -652,84 +618,54 @@ object DeriveJsonEncoder { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { var idx = 0 - while (idx < casts.length) { - val cast = casts(idx) - if (cast.isDefinedAt(a)) { - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - out.write(encodedNames(idx)) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - tcs(idx).unsafeEncode(cast(a), indent_, out) - JsonEncoder.pad(indent, out) - out.write('}') - return - } - idx += 1 - } + while (!casts(idx).isDefinedAt(a)) idx += 1 + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + out.write(encodedNames(idx)) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + tcs(idx).unsafeEncode(casts(idx)(a), indent_, out) + JsonEncoder.pad(indent, out) + out.write('}') } override def toJsonAST(a: A): Either[String, Json] = { var idx = 0 - while (idx < casts.length) { - val cast = casts(idx) - if (cast.isDefinedAt(a)) { - return tcs(idx).toJsonAST(cast(a)).map { inner => - new Json.Obj(Chunk(names(idx) -> inner)) - } - } - idx += 1 - } - throw new IllegalArgumentException // shodn't be reached + while (!casts(idx).isDefinedAt(a)) idx += 1 + tcs(idx).toJsonAST(casts(idx)(a)).map(inner => new Json.Obj(Chunk(names(idx) -> inner))) } } } else { new JsonEncoder[A] { - private[this] val casts = ctx.subtypes.map(_.cast).toArray - private[this] val hintFieldName = discrim.get - private[this] val encodedHintFieldName = { - val out = new FastStringWrite(64) - JsonEncoder.string.unsafeEncode(hintFieldName, None, out) - out.toString - } + private[this] val casts = ctx.subtypes.map(_.cast).toArray + private[this] val hintFieldName = discrim.get + private[this] val encodedHintFieldName = JsonEncoder.string.encodeJson(hintFieldName, None).toString def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { var idx = 0 - while (idx < casts.length) { - val cast = casts(idx) - if (cast.isDefinedAt(a)) { - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - out.write(encodedHintFieldName) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - out.write(encodedNames(idx)) - // whitespace is always off by 2 spaces at the end, probably not worth fixing - tcs(idx).unsafeEncode(cast(a), indent, new NestedWriter(out, indent_)) - return - } - idx += 1 - } + while (!casts(idx).isDefinedAt(a)) idx += 1 + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + out.write(encodedHintFieldName) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + out.write(encodedNames(idx)) + // whitespace is always off by 2 spaces at the end, probably not worth fixing + tcs(idx).unsafeEncode(casts(idx)(a), indent, new NestedWriter(out, indent_)) } override final def toJsonAST(a: A): Either[String, Json] = { var idx = 0 - while (idx < casts.length) { - val cast = casts(idx) - if (cast.isDefinedAt(a)) { - return tcs(idx).toJsonAST(cast(a)).flatMap { - case o: Json.Obj => - val hintField = hintFieldName -> new Json.Str(names(idx)) - new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first - case _ => - new Left("expected object") - } - } - idx += 1 + while (!casts(idx).isDefinedAt(a)) idx += 1 + tcs(idx).toJsonAST(casts(idx)(a)).flatMap { + case o: Json.Obj => + val hintField = hintFieldName -> new Json.Str(names(idx)) + new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first + case _ => + new Left("expected object") } - throw new IllegalArgumentException // shodn't be reached } } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index b82f1e912..3aad818f2 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -8,7 +8,7 @@ import scala.reflect.* import zio.Chunk import zio.json.JsonDecoder.JsonError import zio.json.ast.Json -import zio.json.internal.{ FastStringWrite, FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } +import zio.json.internal.{ FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } import scala.annotation._ import scala.collection.Factory @@ -229,52 +229,40 @@ private class CaseObjectDecoder[Typeclass[*], A](val ctx: CaseClass[Typeclass, A sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonDecoder] { self => def join[A](ctx: CaseClass[Typeclass, A]): JsonDecoder[A] = { - val (transformNames, nameTransform): (Boolean, String => String) = - ctx.annotations.collectFirst { case jsonMemberNames(format) => format } - .orElse(Some(config.fieldNameMapping)) - .filter(_ != IdentityFormat) - .map(true -> _) - .getOrElse(false -> identity) - + val nameTransform: String => String = + ctx.annotations.collectFirst { case jsonMemberNames(format) => format }.getOrElse(config.fieldNameMapping) val no_extra = ctx.annotations.collectFirst { case _: jsonNoExtraFields => () }.isDefined || !config.allowExtraFields - - if (ctx.params.isEmpty) { - new CaseObjectDecoder(ctx, no_extra) - } else { - new CollectionJsonDecoder[A] { - private val (names, aliases): (Array[String], Array[(String, Int)]) = { - val names = new Array[String](ctx.params.size) - val aliasesBuilder = Array.newBuilder[(String, Int)] - ctx.params.foreach { - var idx = 0 - p => - names(idx) = p - .annotations - .collectFirst { case jsonField(name) => name } - .getOrElse(if (transformNames) nameTransform(p.label) else p.label) - aliasesBuilder ++= p - .annotations - .flatMap { - case jsonAliases(alias, aliases*) => (alias +: aliases).map(_ -> idx) - case _ => Seq.empty - } - idx += 1 - } - val aliases = aliasesBuilder.result() - val allFieldNames = names ++ aliases.map(_._1) - if (allFieldNames.length != allFieldNames.distinct.length) { - val aliasNames = aliases.map(_._1) - val collisions = aliasNames - .filter(alias => names.contains(alias) || aliases.count { case (a, _) => a == alias } > 1) - .distinct - val msg = s"Field names and aliases in case class ${ctx.typeInfo.full} must be distinct, " + - s"alias(es) ${collisions.mkString(",")} collide with a field or another alias" - throw new AssertionError(msg) - } - (names, aliases) + if (ctx.params.isEmpty) new CaseObjectDecoder(ctx, no_extra) + else { + val (names, aliases): (Array[String], Array[(String, Int)]) = { + val names = new Array[String](ctx.params.size) + val aliasesBuilder = Array.newBuilder[(String, Int)] + ctx.params.foreach { + var idx = 0 + p => + names(idx) = p.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(p.label)) + aliasesBuilder ++= p.annotations.flatMap { + case jsonAliases(alias, aliases*) => (alias +: aliases).map(_ -> idx) + case _ => Seq.empty + } + idx += 1 } + val aliases = aliasesBuilder.result() + val allFieldNames = names ++ aliases.map(_._1) + if (allFieldNames.length != allFieldNames.distinct.length) { + val aliasNames = aliases.map(_._1) + val collisions = aliasNames + .filter(alias => names.contains(alias) || aliases.count { case (a, _) => a == alias } > 1) + .distinct + val msg = s"Field names and aliases in case class ${ctx.typeInfo.full} must be distinct, " + + s"alias(es) ${collisions.mkString(",")} collide with a field or another alias" + throw new AssertionError(msg) + } + (names, aliases) + } + new CollectionJsonDecoder[A] { private val len = names.length private val matrix = new StringMatrix(names, aliases) private val spans = names.map(JsonError.ObjectAccess(_)) @@ -282,12 +270,10 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv private lazy val tcs = IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap - private val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.decoding }.getOrElse(config.explicitEmptyCollections.decoding) - private val missingValueDecoder = if (explicitEmptyCollections) { lazy val missingValueDecoders = tcs.map { d => @@ -333,15 +319,16 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv if (Lexer.firstField(trace, in)) while({ val idx = Lexer.field(trace, in, matrix) - if (idx != -1) { - if (ps(idx) != null) Lexer.error("duplicate", trace) - val default = defaults(idx) - ps(idx) = if ((default eq null) || in.nextNonWhitespace() != 'n' && { - in.retract() - true - }) tcs(idx).unsafeDecode(spans(idx) :: trace, in) - else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() - else Lexer.error("expected 'null'", spans(idx) :: trace) + if (idx >= 0) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = if ((default eq null) || in.nextNonWhitespace() != 'n' && { + in.retract() + true + }) tcs(idx).unsafeDecode(spans(idx) :: trace, in) + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() + else Lexer.error("expected 'null'", spans(idx) :: trace) + } else Lexer.error("duplicate", trace) } else if (no_extra) Lexer.error("invalid extra field", trace) else Lexer.skipValue(trace, in) Lexer.nextField(trace, in) @@ -366,11 +353,12 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv o.fields.foreach { kv => namesMap.get(kv._1) match { case Some(idx) => - if (ps(idx) != null) Lexer.error("duplicate", trace) - val default = defaults(idx) - ps(idx) = - if ((default ne null) && (kv._2 eq Json.Null)) default() - else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if ((default ne null) && (kv._2 eq Json.Null)) default() + else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) + } else Lexer.error("duplicate", trace) case _ => if (no_extra) Lexer.error("invalid extra field", trace) } @@ -416,7 +404,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv new JsonDecoder[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { val idx = Lexer.enumeration(trace, in, matrix) - if (idx != -1) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + if (idx >= 0) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) else Lexer.error("invalid enumeration value", trace) } @@ -438,7 +426,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv Lexer.char(trace, in, '{') if (Lexer.firstField(trace, in)) { val idx = Lexer.field(trace, in, matrix) - if (idx != -1) { + if (idx >= 0) { val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] Lexer.char(trace, in, '}') a @@ -468,11 +456,12 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv Lexer.char(trace, in_, '{') if (Lexer.firstField(trace, in_)) { while ({ - if (Lexer.field(trace, in_, hintmatrix) != -1) { + if (Lexer.field(trace, in_, hintmatrix) >= 0) { val idx = Lexer.enumeration(trace, in_, matrix) - if (idx == -1) Lexer.error("invalid disambiguator", trace) - in_.rewind() - return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] + if (idx >= 0) { + in_.rewind() + return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] + } else Lexer.error("invalid disambiguator", trace) } else Lexer.skipValue(trace, in_) Lexer.nextField(trace, in_) }) () @@ -531,34 +520,20 @@ object DeriveJsonDecoder extends JsonDecoderDerivation(JsonCodecConfiguration.de sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonEncoder] { self => def join[A](ctx: CaseClass[Typeclass, A]): JsonEncoder[A] = - if (ctx.params.isEmpty) { - caseObjectEncoder.narrow[A] - } else { + if (ctx.params.isEmpty) caseObjectEncoder.narrow[A] + else { new JsonEncoder[A] { - private val (transformNames, nameTransform): (Boolean, String => String) = ctx.annotations - .collectFirst { case jsonMemberNames(format) => format } - .orElse(Some(config.fieldNameMapping)) - .filter(_ != IdentityFormat) - .map(true -> _) - .getOrElse(false -> identity) + private val nameTransform = + ctx.annotations.collectFirst { case jsonMemberNames(format) => format }.getOrElse(config.fieldNameMapping) private val params = IArray.genericWrapArray(ctx.params.filterNot { param => param.annotations.collectFirst { case _: jsonExclude => () }.isDefined }).toArray - private val names = params.map { p => - p.annotations.collectFirst { - case jsonField(name) => name - }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) - }.toArray - private val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) private val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.encoding }.getOrElse(config.explicitEmptyCollections.encoding) - private lazy val fields: Array[FieldEncoder[Any, CaseClass.Param[JsonEncoder, A]]] = params.map { p => - val name = p.annotations.collectFirst { case jsonField(name) => - name - }.getOrElse(if (transformNames) nameTransform(p.label) else p.label) + val name = p.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(p.label)) val withExplicitNulls = explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) val withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.encoding @@ -586,26 +561,27 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv var prevFields = false while (idx < fields.length) { val field = fields(idx) - val p = field.p.deref(a) + idx += 1 val encoder = field.encoder + val p = field.p.deref(a) if ({ (field.flags: @switch) match { - case 0 => !encoder.isEmpty(p) && !encoder.isNothing(p) - case 1 => !encoder.isNothing(p) - case 2 => !encoder.isEmpty(p) - case _ => true + case 0 => encoder.isEmpty(p) || encoder.isNothing(p) + case 1 => encoder.isNothing(p) + case 2 => encoder.isEmpty(p) + case _ => false } - }) { + }) () + else { if (prevFields) { out.write(',') JsonEncoder.pad(indent_, out) } else prevFields = true - JsonEncoder.string.unsafeEncode(field.name, indent_, out) + out.write(field.encodedName) if (indent.isEmpty) out.write(':') else out.write(" : ") encoder.unsafeEncode(p, indent_, out) } - idx += 1 } JsonEncoder.pad(indent, out) out.write('}') @@ -634,11 +610,7 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv val names: Array[String] = IArray.genericWrapArray(ctx.subtypes.map { p => p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeInfo.short)) }).toArray - val encodedNames: Array[String] = names.map { name => - val out = new FastStringWrite(64) - JsonEncoder.string.unsafeEncode(name, None, out) - out.toString - } + val encodedNames: Array[String] = names.map(name => JsonEncoder.string.encodeJson(name, None).toString) lazy val tcs = IArray.genericWrapArray(ctx.subtypes.map(_.typeclass)).toArray.asInstanceOf[Array[JsonEncoder[Any]]] val discrim = @@ -652,24 +624,14 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { var idx = 0 - while (idx < casts.length) { - if (casts(idx).isDefinedAt(a)) { - out.write(encodedNames(idx)) - return - } - idx += 1 - } + while (!casts(idx).isDefinedAt(a)) idx += 1 + out.write(encodedNames(idx)) } override final def toJsonAST(a: A): Either[String, Json] = { var idx = 0 - while (idx < casts.length) { - if (casts(idx).isDefinedAt(a)) { - return new Right(new Json.Str(names(idx))) - } - idx += 1 - } - throw new IllegalArgumentException // shodn't be reached + while (!casts(idx).isDefinedAt(a)) idx += 1 + new Right(new Json.Str(names(idx))) } } } else if (discrim.isEmpty) { @@ -678,84 +640,54 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { var idx = 0 - while (idx < casts.length) { - val cast = casts(idx) - if (cast.isDefinedAt(a)) { - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - out.write(encodedNames(idx)) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - tcs(idx).unsafeEncode(cast(a), indent_, out) - JsonEncoder.pad(indent, out) - out.write('}') - return - } - idx += 1 - } + while (!casts(idx).isDefinedAt(a)) idx += 1 + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + out.write(encodedNames(idx)) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + tcs(idx).unsafeEncode(casts(idx)(a), indent_, out) + JsonEncoder.pad(indent, out) + out.write('}') } override def toJsonAST(a: A): Either[String, Json] = { var idx = 0 - while (idx < casts.length) { - val cast = casts(idx) - if (cast.isDefinedAt(a)) { - return tcs(idx).toJsonAST(cast(a)).map { inner => - new Json.Obj(Chunk(names(idx) -> inner)) - } - } - idx += 1 - } - throw new IllegalArgumentException // shodn't be reached + while (!casts(idx).isDefinedAt(a)) idx += 1 + tcs(idx).toJsonAST(casts(idx)(a)).map(inner => new Json.Obj(Chunk(names(idx) -> inner))) } } } else { new JsonEncoder[A] { private val casts = IArray.genericWrapArray(ctx.subtypes.map(_.cast)).toArray private val hintFieldName = discrim.get - private val encodedHintFieldName = { - val out = new FastStringWrite(64) - JsonEncoder.string.unsafeEncode(hintFieldName, None, out) - out.toString - } + private val encodedHintFieldName = JsonEncoder.string.encodeJson(hintFieldName, None).toString def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { var idx = 0 - while (idx < casts.length) { - val cast = casts(idx) - if (cast.isDefinedAt(a)) { - out.write('{') - val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - out.write(encodedHintFieldName) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - out.write(encodedNames(idx)) - // whitespace is always off by 2 spaces at the end, probably not worth fixing - tcs(idx).unsafeEncode(cast(a), indent, new DeriveJsonEncoder.NestedWriter(out, indent_)) - return - } - idx += 1 - } + while (!casts(idx).isDefinedAt(a)) idx += 1 + out.write('{') + val indent_ = JsonEncoder.bump(indent) + JsonEncoder.pad(indent_, out) + out.write(encodedHintFieldName) + if (indent.isEmpty) out.write(':') + else out.write(" : ") + out.write(encodedNames(idx)) + // whitespace is always off by 2 spaces at the end, probably not worth fixing + tcs(idx).unsafeEncode(casts(idx)(a), indent, new DeriveJsonEncoder.NestedWriter(out, indent_)) } override final def toJsonAST(a: A): Either[String, Json] = { var idx = 0 - while (idx < casts.length) { - val cast = casts(idx) - if (cast.isDefinedAt(a)) { - return tcs(idx).toJsonAST(cast(a)).flatMap { - case o: Json.Obj => - val hintField = hintFieldName -> new Json.Str(names(idx)) - new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first - case _ => - new Left("expected object") - } - } - idx += 1 + while (!casts(idx).isDefinedAt(a)) idx += 1 + tcs(idx).toJsonAST(casts(idx)(a)).flatMap { + case o: Json.Obj => + val hintField = hintFieldName -> new Json.Str(names(idx)) + new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first + case _ => + new Left("expected object") } - throw new IllegalArgumentException // shodn't be reached } } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index e770dc5eb..146712994 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -82,11 +82,11 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { * Note: This method may not entirely consume the specified character sequence. */ final def decodeJson(str: CharSequence): Either[String, A] = - try Right(unsafeDecode(Nil, new FastStringReader(str))) + try new Right(unsafeDecode(Nil, new FastStringReader(str))) catch { - case JsonDecoder.UnsafeJson(trace) => Left(JsonError.render(trace)) - case _: UnexpectedEnd => Left("Unexpected end of input") - case _: StackOverflowError => Left("Unexpected structure") + case e: JsonDecoder.UnsafeJson => new Left(JsonError.render(e.trace)) + case _: UnexpectedEnd => new Left("Unexpected end of input") + case _: StackOverflowError => new Left("Unexpected structure") } /** @@ -120,7 +120,7 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { } } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A1 = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): A1 = try self.unsafeFromJsonAST(trace, json) catch { case _: JsonDecoder.UnsafeJson | _: UnexpectedEnd => that.unsafeFromJsonAST(trace, json) @@ -129,7 +129,7 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { override def unsafeDecodeMissing(trace: List[JsonError]): A1 = try self.unsafeDecodeMissing(trace) catch { - case _: Throwable => that.unsafeDecodeMissing(trace) + case _: JsonDecoder.UnsafeJson | _: UnexpectedEnd => that.unsafeDecodeMissing(trace) } } @@ -149,9 +149,7 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { def unsafeDecode(trace: List[JsonError], in: RetractReader): B = f(self.unsafeDecode(trace, in)) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): B = f( - self.unsafeFromJsonAST(trace, json) - ) + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): B = f(self.unsafeFromJsonAST(trace, json)) override def unsafeDecodeMissing(trace: List[JsonError]): B = f(self.unsafeDecodeMissing(trace)) } @@ -170,7 +168,7 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { case Left(err) => Lexer.error(err, trace) } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): B = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): B = f(self.unsafeFromJsonAST(trace, json)) match { case Right(b) => b case Left(err) => Lexer.error(err, trace) @@ -222,11 +220,11 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { * more performant implementation. */ final def fromJsonAST(json: Json): Either[String, A] = - try Right(unsafeFromJsonAST(Nil, json)) + try new Right(unsafeFromJsonAST(Nil, json)) catch { - case JsonDecoder.UnsafeJson(trace) => Left(JsonError.render(trace)) - case _: UnexpectedEnd => Left("Unexpected end of input") - case _: StackOverflowError => Left("Unexpected structure") + case e: JsonDecoder.UnsafeJson => new Left(JsonError.render(e.trace)) + case _: UnexpectedEnd => new Left("Unexpected end of input") + case _: StackOverflowError => new Left("Unexpected structure") } } @@ -271,7 +269,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with implicit val string: JsonDecoder[String] = new JsonDecoder[String] { def unsafeDecode(trace: List[JsonError], in: RetractReader): String = Lexer.string(trace, in).toString - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): String = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): String = json match { case s: Json.Str => s.value case _ => Lexer.error("expected string", trace) @@ -281,7 +279,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with implicit val boolean: JsonDecoder[Boolean] = new JsonDecoder[Boolean] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Boolean = Lexer.boolean(trace, in) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Boolean = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Boolean = json match { case b: Json.Bool => b.value case _ => Lexer.error("expected boolean", trace) @@ -291,7 +289,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with implicit val char: JsonDecoder[Char] = new JsonDecoder[Char] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Char = Lexer.char(trace, in) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Char = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Char = json match { case s: Json.Str if s.value.length == 1 => s.value.charAt(0) case _ => Lexer.error("expected single character string", trace) @@ -312,7 +310,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Byte = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Byte = json match { case n: Json.Num => try n.value.byteValueExact @@ -336,7 +334,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Short = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Short = json match { case n: Json.Num => try n.value.shortValueExact @@ -360,7 +358,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Int = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Int = json match { case n: Json.Num => try n.value.intValueExact @@ -383,7 +381,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Long = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Long = json match { case n: Json.Num => try n.value.longValueExact @@ -407,7 +405,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigInteger = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigInteger = json match { case n: Json.Num => try n.value.toBigIntegerExact @@ -430,7 +428,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigInt = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigInt = json match { case n: Json.Num => try BigInt(n.value.toBigIntegerExact) @@ -453,7 +451,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Float = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Float = json match { case n: Json.Num => n.value.floatValue case s: Json.Str => Lexer.float(trace, new FastStringReader(s.value)) @@ -472,7 +470,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Double = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Double = json match { case n: Json.Num => n.value.doubleValue case s: Json.Str => Lexer.double(trace, new FastStringReader(s.value)) @@ -491,7 +489,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigDecimal = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigDecimal = json match { case n: Json.Num => n.value case s: Json.Str => Lexer.bigDecimal(trace, new FastStringReader(s.value)) @@ -510,7 +508,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigDecimal = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigDecimal = json match { case n: Json.Num => new BigDecimal(n.value, BigDecimal.defaultMathContext) case s: Json.Str => Lexer.bigDecimal(trace, new FastStringReader(s.value)) @@ -527,19 +525,15 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override def unsafeDecodeMissing(trace: List[JsonError]): Option[A] = None def unsafeDecode(trace: List[JsonError], in: RetractReader): Option[A] = - if (in.nextNonWhitespace() == 'n') { - if (in.readChar() != 'u' || in.readChar() != 'l' || in.readChar() != 'l') { - Lexer.error("expected 'null'", trace) - } - None - } else { + if (in.nextNonWhitespace() != 'n') { in.retract() new Some(A.unsafeDecode(trace, in)) - } + } else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') None + else Lexer.error("expected 'null'", trace) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Option[A] = - if (json eq Json.Null) None - else new Some(A.unsafeFromJsonAST(trace, json)) + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Option[A] = + if (json ne Json.Null) new Some(A.unsafeFromJsonAST(trace, json)) + else None } // supports multiple representations for compatibility with other libraries, @@ -585,14 +579,16 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with builder: mutable.Builder[A, T[A]] )(implicit A: JsonDecoder[A]): T[A] = { val c = in.nextNonWhitespace() - if (c != '[') Lexer.error("'['", c, trace) - var i: Int = 0 - if (Lexer.firstArrayElement(in)) while ({ - builder += A.unsafeDecode(new JsonError.ArrayAccess(i) :: trace, in) - i += 1 - Lexer.nextArrayElement(trace, in) - }) () - builder.result() + if (c == '[') { + var i = 0 + if (Lexer.firstArrayElement(in)) while ({ + builder += A.unsafeDecode(new JsonError.ArrayAccess(i) :: trace, in) + i += 1 + Lexer.nextArrayElement(trace, in) + }) () + return builder.result() + } + Lexer.error("'['", c, trace) } @inline private[json] def keyValueBuilder[K, V, T[X, Y] <: Iterable[(X, Y)]]( @@ -601,18 +597,20 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with builder: mutable.Builder[(K, V), T[K, V]] )(implicit K: JsonFieldDecoder[K], V: JsonDecoder[V]): T[K, V] = { var c = in.nextNonWhitespace() - if (c != '{') Lexer.error("'{'", c, trace) - if (Lexer.firstField(trace, in)) - while ({ - val field = Lexer.string(trace, in).toString - val trace_ = new JsonError.ObjectAccess(field) :: trace - c = in.nextNonWhitespace() - if (c != ':') Lexer.error("':'", c, trace) - val value = V.unsafeDecode(trace_, in) - builder += ((K.unsafeDecodeField(trace_, field), value)) - Lexer.nextField(trace, in) - }) () - builder.result() + if (c == '{') { + if (Lexer.firstField(trace, in)) + while ({ + val field = Lexer.string(trace, in).toString + val trace_ = new JsonError.ObjectAccess(field) :: trace + c = in.nextNonWhitespace() + if (c != ':') Lexer.error("':'", c, trace) + val value = V.unsafeDecode(trace_, in) + builder += ((K.unsafeDecodeField(trace_, field), value)) + Lexer.nextField(trace, in) + }) () + return builder.result() + } + Lexer.error("'{'", c, trace) } // FIXME: remove in the next major version @@ -647,29 +645,29 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { def unsafeDecode(trace: List[JsonError], in: RetractReader): Array[A] = { val c = in.nextNonWhitespace() - if (c != '[') Lexer.error("'['", c, trace) - if (Lexer.firstArrayElement(in)) { - var l = 8 - var x = new Array[A](l) - var i = 0 - while ({ - if (i == l) { - l <<= 1 - val x1 = new Array[A](l) - System.arraycopy(x, 0, x1, 0, i) - x = x1 - } - x(i) = A.unsafeDecode(new JsonError.ArrayAccess(i) :: trace, in) - i += 1 - Lexer.nextArrayElement(trace, in) - }) () - if (i != l) { + if (c == '[') { + if (Lexer.firstArrayElement(in)) { + var l = 8 + var x = new Array[A](l) + var i = 0 + while ({ + if (i == l) { + l <<= 1 + val x1 = new Array[A](l) + System.arraycopy(x, 0, x1, 0, i) + x = x1 + } + x(i) = A.unsafeDecode(new JsonError.ArrayAccess(i) :: trace, in) + i += 1 + Lexer.nextArrayElement(trace, in) + }) () + if (i == l) return x val x1 = new Array[A](i) - _root_.java.lang.System.arraycopy(x, 0, x1, 0, i) - x = x1 - } - x - } else Array.empty + System.arraycopy(x, 0, x1, 0, i) + return x1 + } else return Array.empty + } + Lexer.error("'['", c, trace) } } @@ -690,7 +688,7 @@ private[json] trait DecoderLowPriority1 extends DecoderLowPriority2 { def unsafeDecode(trace: List[JsonError], in: RetractReader): Chunk[A] = builder(trace, in, zio.ChunkBuilder.make[A]()) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Chunk[A] = json match { case a: Json.Arr => a.elements.map { @@ -882,7 +880,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { def unsafeDecode(trace: List[JsonError], in: RetractReader): A = parseJavaTime(trace, Lexer.string(trace, in).toString) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = json match { case s: Json.Str => parseJavaTime(trace, s.value) case _ => Lexer.error("expected string", trace) @@ -912,7 +910,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { implicit val uuid: JsonDecoder[UUID] = new JsonDecoder[UUID] { def unsafeDecode(trace: List[JsonError], in: RetractReader): UUID = Lexer.uuid(trace, in) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID = json match { case s: Json.Str => try UUIDParser.unsafeParse(s.value) @@ -927,7 +925,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { def unsafeDecode(trace: List[JsonError], in: RetractReader): java.util.Currency = parseCurrency(trace, Lexer.string(trace, in).toString) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.util.Currency = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.util.Currency = json match { case s: Json.Str => parseCurrency(trace, s.value) case _ => Lexer.error("expected string", trace) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 14204da23..dd66311c6 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -136,7 +136,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with write } - def release(): Unit = level -= 1 // decrease the level of recusrion + def release(): Unit = if (level > 0) level -= 1 // decrease the level of recusrion } private val writePools = new ThreadLocal[FastStringWritePool] { @@ -162,7 +162,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: String): Either[String, Json] = new Right(new Json.Str(a)) + @inline override final def toJsonAST(a: String): Either[String, Json] = new Right(new Json.Str(a)) private[this] def writeEncoded(a: String, out: Write): Unit = { val len = a.length diff --git a/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala b/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala index 73dc5034e..bad2a4d7c 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala @@ -11,19 +11,17 @@ private[json] class FieldEncoder[T, P]( val encoder: JsonEncoder[T], val flags: Int ) { + val encodedName: String = JsonEncoder.string.encodeJson(name, None).toString + def encodeOrDefault(t: T)( encode: () => Either[String, Chunk[(String, Json)]], default: Either[String, Chunk[(String, Json)]] ): Either[String, Chunk[(String, Json)]] = (flags: @switch) match { - case 0 => - if (!encoder.isEmpty(t) && !encoder.isNothing(t)) encode() else default - case 1 => - if (!encoder.isNothing(t)) encode() else default - case 2 => - if (!encoder.isEmpty(t)) encode() else default - case _ => - encode() + case 0 => if (encoder.isEmpty(t) || encoder.isNothing(t)) default else encode() + case 1 => if (encoder.isNothing(t)) default else encode() + case 2 => if (encoder.isEmpty(t)) default else encode() + case _ => encode() } } @@ -41,8 +39,9 @@ private[json] object FieldEncoder { encoder, { if (withExplicitNulls) { if (withExplicitEmptyCollections) 3 else 2 - } else if (withExplicitEmptyCollections) 1 - else 0 + } else { + if (withExplicitEmptyCollections) 1 else 0 + } } ) } diff --git a/zio-json/shared/src/main/scala/zio/json/package.scala b/zio-json/shared/src/main/scala/zio/json/package.scala index 7ec99fd4a..deca80500 100644 --- a/zio-json/shared/src/main/scala/zio/json/package.scala +++ b/zio-json/shared/src/main/scala/zio/json/package.scala @@ -19,12 +19,12 @@ import zio.json.ast.Json package object json extends JsonPackagePlatformSpecific { implicit final class EncoderOps[A](private val a: A) extends AnyVal { - def toJson(implicit encoder: JsonEncoder[A]): String = encoder.encodeJson(a, None).toString + @inline def toJson(implicit encoder: JsonEncoder[A]): String = encoder.encodeJson(a, None).toString // Jon Pretty's better looking brother, but a bit slower - def toJsonPretty(implicit encoder: JsonEncoder[A]): String = encoder.encodeJson(a, Some(0)).toString + @inline def toJsonPretty(implicit encoder: JsonEncoder[A]): String = encoder.encodeJson(a, Some(0)).toString - def toJsonAST(implicit encoder: JsonEncoder[A]): Either[String, Json] = encoder.toJsonAST(a) + @inline def toJsonAST(implicit encoder: JsonEncoder[A]): Either[String, Json] = encoder.toJsonAST(a) } implicit final class DecoderOps(private val json: CharSequence) extends AnyVal { @@ -38,6 +38,6 @@ package object json extends JsonPackagePlatformSpecific { * * {{{jq '.rows[0].elements[0].distance' input.json}}} */ - def fromJson[A](implicit decoder: JsonDecoder[A]): Either[String, A] = decoder.decodeJson(json) + @inline def fromJson[A](implicit decoder: JsonDecoder[A]): Either[String, A] = decoder.decodeJson(json) } } From f6ace96a1bec5f20e19ac312dcf3a12aff40d628 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Wed, 26 Feb 2025 06:15:06 +0100 Subject: [PATCH 191/311] Update magnolia to 1.3.15 (#1342) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 7da8d25d8..8f8b54c52 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Seq( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.14" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.15" ) case _ => Seq( From 0dd96c4c691a3daea9508c8019fb40b5a6c6f761 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 26 Feb 2025 12:11:53 +0100 Subject: [PATCH 192/311] Fix unwanted skipping of required fields that has values of product types + yet more efficient encoding of product types (#1343) --- .../src/main/scala-2.x/zio/json/macros.scala | 77 +++++++-------- .../src/main/scala-3/zio/json/macros.scala | 93 ++++++++----------- .../zio/json/internal/FieldEncoder.scala | 36 ++++--- .../json/ConfigurableDeriveCodecSpec.scala | 8 +- .../internal/FieldEncoderHelperSpec.scala | 47 ++-------- 5 files changed, 102 insertions(+), 159 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index c54f81f8f..9955a9c0f 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -507,80 +507,67 @@ object DeriveJsonEncoder { else { val nameTransform = ctx.annotations.collectFirst { case jsonMemberNames(format) => format }.getOrElse(config.fieldNameMapping) - val params = ctx.parameters.filter(p => p.annotations.collectFirst { case _: jsonExclude => () }.isEmpty).toArray val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.encoding } .getOrElse(config.explicitEmptyCollections.encoding) + val params = ctx.parameters.filter(p => p.annotations.collectFirst { case _: jsonExclude => () }.isEmpty).toArray new JsonEncoder[A] { - private[this] lazy val fields: Array[FieldEncoder[Any, Param[JsonEncoder, A]]] = params.map { p => - val name = p.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(p.label)) - val withExplicitNulls = explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - val withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => - a.encoding - }.getOrElse(explicitEmptyCollections) + private[this] lazy val fields = params.map { p => FieldEncoder( - p, - name, - p.typeclass.asInstanceOf[JsonEncoder[Any]], - withExplicitNulls = withExplicitNulls, - withExplicitEmptyCollections = withExplicitEmptyCollections + p = p, + name = p.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(p.label)), + encoder = p.typeclass.asInstanceOf[JsonEncoder[Any]], + withExplicitNulls = explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]), + withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.encoding + }.getOrElse(explicitEmptyCollections) ) } - override def isEmpty(a: A): Boolean = fields.forall { field => - val paramValue = field.p.dereference(a) - field.encoder.isEmpty(paramValue) || field.encoder.isNothing(paramValue) - } - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { out.write('{') val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) val fields = this.fields var idx = 0 - var prevFields = false // whether any fields have been written + var prevFields = false while (idx < fields.length) { val field = fields(idx) idx += 1 - val encoder = field.encoder - val p = field.p.dereference(a) - if ({ - (field.flags: @switch) match { - case 0 => encoder.isEmpty(p) || encoder.isNothing(p) - case 1 => encoder.isNothing(p) - case 2 => encoder.isEmpty(p) - case _ => false - } - }) () + val p = field.p.dereference(a) + if (field.skip(p)) () else { if (prevFields) { out.write(',') JsonEncoder.pad(indent_, out) } else prevFields = true - out.write(field.encodedName) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - encoder.unsafeEncode(p, indent_, out) + if (indent.isEmpty) out.write(field.encodedName) + else out.write(field.prettyEncodedName) + field.encoder.unsafeEncode(p, indent_, out) } } JsonEncoder.pad(indent, out) out.write('}') } - override final def toJsonAST(a: A): Either[String, Json] = - fields - .foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, field) => - val param = field.p - val paramValue = param.dereference(a).asInstanceOf[param.PType] - field.encodeOrDefault(paramValue)( - () => - c.flatMap { chunk => - param.typeclass.toJsonAST(paramValue).map(value => chunk :+ field.name -> value) - }, - c - ) + override final def toJsonAST(a: A): Either[String, Json] = { + val buf = Array.newBuilder[(String, Json)] + val fields = this.fields + var idx = 0 + while (idx < fields.length) { + val field = fields(idx) + idx += 1 + val p = field.p.dereference(a) + if (field.skip(p)) () + else { + field.encoder.toJsonAST(p) match { + case Right(value) => buf += ((field.name, value)) + case _ => + } } - .map(Json.Obj.apply) + } + new Right(Json.Obj(Chunk.fromArray(buf.result()))) + } } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 3aad818f2..b080b1b4b 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -519,39 +519,31 @@ object DeriveJsonDecoder extends JsonDecoderDerivation(JsonCodecConfiguration.de } sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Derivation[JsonEncoder] { self => - def join[A](ctx: CaseClass[Typeclass, A]): JsonEncoder[A] = + def join[A](ctx: CaseClass[Typeclass, A]): JsonEncoder[A] = if (ctx.params.isEmpty) caseObjectEncoder.narrow[A] else { + val nameTransform = + ctx.annotations.collectFirst { case jsonMemberNames(format) => format }.getOrElse(config.fieldNameMapping) + val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.encoding + }.getOrElse(config.explicitEmptyCollections.encoding) + val params = IArray.genericWrapArray(ctx.params.filterNot { param => + param.annotations.collectFirst { case _: jsonExclude => () }.isDefined + }).toArray new JsonEncoder[A] { - private val nameTransform = - ctx.annotations.collectFirst { case jsonMemberNames(format) => format }.getOrElse(config.fieldNameMapping) - private val params = IArray.genericWrapArray(ctx.params.filterNot { param => - param.annotations.collectFirst { case _: jsonExclude => () }.isDefined - }).toArray - private val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - private val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => - a.encoding - }.getOrElse(config.explicitEmptyCollections.encoding) - private lazy val fields: Array[FieldEncoder[Any, CaseClass.Param[JsonEncoder, A]]] = params.map { p => - val name = p.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(p.label)) - val withExplicitNulls = explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]) - val withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => - a.encoding - }.getOrElse(explicitEmptyCollections) + private lazy val fields = params.map { p => FieldEncoder( - p, - name, - p.typeclass.asInstanceOf[JsonEncoder[Any]], - withExplicitNulls = withExplicitNulls, - withExplicitEmptyCollections = withExplicitEmptyCollections + p = p, + name = p.annotations.collectFirst { case jsonField(name) => name }.getOrElse(nameTransform(p.label)), + encoder = p.typeclass.asInstanceOf[JsonEncoder[Any]], + withExplicitNulls = explicitNulls || p.annotations.exists(_.isInstanceOf[jsonExplicitNull]), + withExplicitEmptyCollections = p.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.encoding + }.getOrElse(explicitEmptyCollections) ) } - override def isEmpty(a: A): Boolean = fields.forall { field => - val paramValue = field.p.deref(a) - field.encoder.isEmpty(paramValue) || field.encoder.isNothing(paramValue) - } - def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { out.write('{') val indent_ = JsonEncoder.bump(indent) @@ -562,45 +554,40 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv while (idx < fields.length) { val field = fields(idx) idx += 1 - val encoder = field.encoder - val p = field.p.deref(a) - if ({ - (field.flags: @switch) match { - case 0 => encoder.isEmpty(p) || encoder.isNothing(p) - case 1 => encoder.isNothing(p) - case 2 => encoder.isEmpty(p) - case _ => false - } - }) () + val p = field.p.deref(a) + if (field.skip(p)) () else { if (prevFields) { out.write(',') JsonEncoder.pad(indent_, out) } else prevFields = true - out.write(field.encodedName) - if (indent.isEmpty) out.write(':') - else out.write(" : ") - encoder.unsafeEncode(p, indent_, out) + if (indent.isEmpty) out.write(field.encodedName) + else out.write(field.prettyEncodedName) + field.encoder.unsafeEncode(p, indent_, out) } } JsonEncoder.pad(indent, out) out.write('}') } - override final def toJsonAST(a: A): Either[String, Json] = - fields - .foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (c, field) => - val param = field.p - val paramValue = param.deref(a) - field.encodeOrDefault(paramValue)( - () => - c.flatMap { chunk => - param.typeclass.toJsonAST(paramValue).map(value => chunk :+ field.name -> value) - }, - c - ) + override final def toJsonAST(a: A): Either[String, Json] = { + val buf = Array.newBuilder[(String, Json)] + val fields = this.fields + var idx = 0 + while (idx < fields.length) { + val field = fields(idx) + idx += 1 + val p = field.p.deref(a) + if (field.skip(p)) () + else { + field.encoder.toJsonAST(p) match { + case Right(value) => buf += ((field.name, value)) + case _ => + } } - .map(Json.Obj.apply) + } + new Right(Json.Obj(Chunk.fromArray(buf.result()))) + } } } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala b/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala index bad2a4d7c..40559a7c8 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/FieldEncoder.scala @@ -1,28 +1,22 @@ package zio.json.internal -import zio.Chunk import zio.json._ -import zio.json.ast.Json import scala.annotation.switch private[json] class FieldEncoder[T, P]( val p: P, - val name: String, val encoder: JsonEncoder[T], - val flags: Int + val encodedName: String, + val prettyEncodedName: String, + val name: String, + private[this] val flags: Int ) { - val encodedName: String = JsonEncoder.string.encodeJson(name, None).toString - - def encodeOrDefault(t: T)( - encode: () => Either[String, Chunk[(String, Json)]], - default: Either[String, Chunk[(String, Json)]] - ): Either[String, Chunk[(String, Json)]] = - (flags: @switch) match { - case 0 => if (encoder.isEmpty(t) || encoder.isNothing(t)) default else encode() - case 1 => if (encoder.isNothing(t)) default else encode() - case 2 => if (encoder.isEmpty(t)) default else encode() - case _ => encode() - } + def skip(t: T): Boolean = (flags: @switch) match { + case 0 => encoder.isEmpty(t) || encoder.isNothing(t) + case 1 => encoder.isNothing(t) + case 2 => encoder.isEmpty(t) + case _ => false + } } private[json] object FieldEncoder { @@ -32,11 +26,14 @@ private[json] object FieldEncoder { encoder: JsonEncoder[T], withExplicitNulls: Boolean, withExplicitEmptyCollections: Boolean - ): FieldEncoder[T, P] = + ): FieldEncoder[T, P] = { + val encodedName = JsonEncoder.string.encodeJson(name, None).toString new FieldEncoder( p, - name, - encoder, { + encoder, + encodedName + ':', + encodedName + " : ", + name, { if (withExplicitNulls) { if (withExplicitEmptyCollections) 3 else 2 } else { @@ -44,4 +41,5 @@ private[json] object FieldEncoder { } } ) + } } diff --git a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala index a1ba89e22..1ed90a175 100644 --- a/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ConfigurableDeriveCodecSpec.scala @@ -258,7 +258,7 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { case class EmptyObj(a: Empty) case class EmptySeq(b: Seq[Int]) - val expectedStr = """{}""" + val expectedStr = """{"a":{}}""" val expectedEmptyObj = EmptyObj(Empty(None)) val expectedEmptySeq = EmptySeq(Seq.empty) @@ -271,7 +271,7 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { assertTrue( expectedEmptyObj.toJson == expectedStr, - expectedEmptySeq.toJson == expectedStr, + expectedEmptySeq.toJson == "{}", expectedStr.fromJson[EmptyObj] == Right(expectedEmptyObj), expectedStr.fromJson[EmptySeq] == Right(expectedEmptySeq) ) @@ -357,7 +357,7 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { case class EmptyObj(a: Empty) case class EmptySeq(b: Seq[Int]) - val expectedJson = Json.Obj() + val expectedJson = Json.Obj(Chunk("a" -> Json.Obj.empty)) val expectedEmptyObj = EmptyObj(Empty(None)) val expectedEmptySeq = EmptySeq(Seq.empty) @@ -370,7 +370,7 @@ object ConfigurableDeriveCodecSpec extends ZIOSpecDefault { assertTrue( expectedEmptyObj.toJsonAST == Right(expectedJson), - expectedEmptySeq.toJsonAST == Right(expectedJson), + expectedEmptySeq.toJsonAST == Right(Json.Obj()), expectedJson.as[EmptyObj] == Right(expectedEmptyObj), expectedJson.as[EmptySeq] == Right(expectedEmptySeq) ) diff --git a/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala index a18b61d1d..8dc33fe79 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/FieldEncoderHelperSpec.scala @@ -1,8 +1,6 @@ package zio.json package internal -import zio.json.ast.Json -import zio.Chunk import zio.test._ object FieldEncoderSpec extends ZIOSpecDefault { @@ -17,10 +15,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { withExplicitNulls = false, withExplicitEmptyCollections = false ) - val expected = Chunk(("a", Json.Bool.True)) - assertTrue( - helper.encodeOrDefault(None)(() => Left(""), Right(expected)) == Right(expected) - ) + assertTrue(helper.skip(None)) }, test("should encode None when withExplicitNulls is true") { val helper = FieldEncoder( @@ -30,10 +25,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { withExplicitNulls = true, withExplicitEmptyCollections = false ) - val expected = Chunk(("a", Json.Bool.True)) - assertTrue( - helper.encodeOrDefault(None)(() => Right(expected), Left("")) == Right(expected) - ) + assertTrue(!helper.skip(None)) } ), suite("CollectionEncoder")( @@ -45,10 +37,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { withExplicitNulls = false, withExplicitEmptyCollections = true ) - val expected = Chunk(("a", Json.Bool.True)) - assertTrue( - helper.encodeOrDefault(Nil)(() => Right(expected), Left("")) == Right(expected) - ) + assertTrue(!helper.skip(Nil)) }, test("should not encode empty collections when withExplicitEmptyCollections is false") { val helper = FieldEncoder( @@ -58,10 +47,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { withExplicitNulls = false, withExplicitEmptyCollections = false ) - val expected = Chunk(("a", Json.Bool.True)) - assertTrue( - helper.encodeOrDefault(Nil)(() => Left(""), Right(expected)) == Right(expected) - ) + assertTrue(helper.skip(Nil)) } ), suite("for a case class")( @@ -74,15 +60,9 @@ object FieldEncoderSpec extends ZIOSpecDefault { withExplicitNulls = false, withExplicitEmptyCollections = true ) - val expected = Chunk(("a", Json.Bool.True)) - assertTrue( - helper.encodeOrDefault(Test(Nil, None))( - () => Right(expected), - Left("") - ) == Right(expected) - ) + assertTrue(!helper.skip(Test(Nil, None))) }, - test("should not encode case classes with empty collections when withExplicitEmptyCollections is false") { + test("should encode case classes with empty collections when withExplicitEmptyCollections is false") { case class Test(list: List[Int], option: Option[Int]) val helper = FieldEncoder( 1, @@ -91,13 +71,10 @@ object FieldEncoderSpec extends ZIOSpecDefault { withExplicitNulls = false, withExplicitEmptyCollections = false ) - val expected = Chunk(("a", Json.Bool.True)) - assertTrue( - helper.encodeOrDefault(Test(Nil, None))(() => Left(""), Right(expected)) == Right(expected) - ) + assertTrue(!helper.skip(Test(Nil, None))) }, test( - "should also not encode case classes with empty options when withExplicitEmptyCollections is false, even when withExplicitNulls is true" + "should encode case classes with empty options when withExplicitEmptyCollections is false, even when withExplicitNulls is true" ) { case class Test(list: List[Int], option: Option[Int]) val helper = FieldEncoder( @@ -107,13 +84,7 @@ object FieldEncoderSpec extends ZIOSpecDefault { withExplicitNulls = true, withExplicitEmptyCollections = false ) - val expected = Chunk(("a", Json.Bool.True)) - assertTrue( - helper.encodeOrDefault(Test(Nil, None))( - () => Left(""), - Right(expected) - ) == Right(expected) - ) + assertTrue(!helper.skip(Test(Nil, None))) } ) ) From 1e27996186e3a313226ad93dbe359b891eef24d2 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 26 Feb 2025 13:32:23 +0100 Subject: [PATCH 193/311] Fix `toJsonAST` implementation for product types + More efficient `toJsonAST` implementation for product types and collections + Update magnolia to 1.3.16 (#1344) * Fix implementation of `toJsonAST` for product types * Update magnolia to 1.3.16 * More efficient `toJsonAST` for collections --- build.sbt | 2 +- .../src/main/scala-2.x/zio/json/macros.scala | 14 +- .../src/main/scala-3/zio/json/macros.scala | 14 +- .../src/main/scala/zio/json/JsonEncoder.scala | 137 +++++++++++------- 4 files changed, 106 insertions(+), 61 deletions(-) diff --git a/build.sbt b/build.sbt index 8f8b54c52..b454a1d81 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Seq( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.15" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.16" ) case _ => Seq( diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 9955a9c0f..9f0ae7e41 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -551,9 +551,9 @@ object DeriveJsonEncoder { } override final def toJsonAST(a: A): Either[String, Json] = { - val buf = Array.newBuilder[(String, Json)] val fields = this.fields - var idx = 0 + var buf = new Array[(String, Json)](fields.length) + var i, idx = 0 while (idx < fields.length) { val field = fields(idx) idx += 1 @@ -561,12 +561,16 @@ object DeriveJsonEncoder { if (field.skip(p)) () else { field.encoder.toJsonAST(p) match { - case Right(value) => buf += ((field.name, value)) - case _ => + case Right(value) => + buf(i) = (field.name, value) + i += 1 + case left => + return left } } } - new Right(Json.Obj(Chunk.fromArray(buf.result()))) + if (i != buf.length) buf = java.util.Arrays.copyOf(buf, i) + new Right(Json.Obj(Chunk.fromArray(buf))) } } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index b080b1b4b..8f9cd7a08 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -571,9 +571,9 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv } override final def toJsonAST(a: A): Either[String, Json] = { - val buf = Array.newBuilder[(String, Json)] val fields = this.fields - var idx = 0 + var buf = new Array[(String, Json)](fields.length) + var i, idx = 0 while (idx < fields.length) { val field = fields(idx) idx += 1 @@ -581,12 +581,16 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv if (field.skip(p)) () else { field.encoder.toJsonAST(p) match { - case Right(value) => buf += ((field.name, value)) - case _ => + case Right(value) => + buf(i) = (field.name, value) + i += 1 + case left => + return left } } } - new Right(Json.Obj(Chunk.fromArray(buf.result()))) + if (i != buf.length) buf = java.util.Arrays.copyOf(buf, i) + new Right(Json.Obj(Chunk.fromArray(buf))) } } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index dd66311c6..32d51f9a9 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -38,7 +38,7 @@ trait JsonEncoder[A] extends JsonEncoderPlatformSpecific[A] { override def isEmpty(b: B): Boolean = self.isEmpty(f(b)) - override final def toJsonAST(b: B): Either[String, Json] = self.toJsonAST(f(b)) + override def toJsonAST(b: B): Either[String, Json] = self.toJsonAST(f(b)) } /** @@ -162,7 +162,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - @inline override final def toJsonAST(a: String): Either[String, Json] = new Right(new Json.Str(a)) + @inline override def toJsonAST(a: String): Either[String, Json] = new Right(new Json.Str(a)) private[this] def writeEncoded(a: String, out: Write): Unit = { val len = a.length @@ -208,14 +208,14 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with } } - override final def toJsonAST(a: Char): Either[String, Json] = new Right(new Json.Str(a.toString)) + override def toJsonAST(a: Char): Either[String, Json] = new Right(new Json.Str(a.toString)) } // FIXME: remove in the next major version private[json] def explicit[A](f: A => String, g: A => Json): JsonEncoder[A] = new JsonEncoder[A] { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = out.write(f(a)) - override final def toJsonAST(a: A): Either[String, Json] = new Right(g(a)) + override def toJsonAST(a: A): Either[String, Json] = new Right(g(a)) } // FIXME: remove in the next major version @@ -226,7 +226,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with out.write('"') } - override final def toJsonAST(a: A): Either[String, Json] = new Right(new Json.Str(f(a))) + override def toJsonAST(a: A): Either[String, Json] = new Right(new Json.Str(f(a))) } // FIXME: add tests @@ -248,7 +248,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with if (a) out.write('t', 'r', 'u', 'e') else out.write('f', 'a', 'l', 's', 'e') - override final def toJsonAST(a: Boolean): Either[String, Json] = new Right(Json.Bool(a)) + override def toJsonAST(a: Boolean): Either[String, Json] = new Right(Json.Bool(a)) } implicit val symbol: JsonEncoder[Symbol] = string.contramap(_.name) implicit val byte: JsonEncoder[Byte] = new JsonEncoder[Byte] { @@ -311,7 +311,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def isNothing(oa: Option[A]): Boolean = (oa eq None) || A.isNothing(oa.get) - override final def toJsonAST(oa: Option[A]): Either[String, Json] = + override def toJsonAST(oa: Option[A]): Either[String, Json] = if (oa eq None) new Right(Json.Null) else A.toJsonAST(oa.get) } @@ -368,7 +368,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with pad(indent, out) } - override final def toJsonAST(eab: Either[A, B]): Either[String, Json] = + override def toJsonAST(eab: Either[A, B]): Either[String, Json] = eab match { case Left(a) => A.toJsonAST(a).map(v => Json.Obj(Chunk.single("Left" -> v))) case Right(b) => B.toJsonAST(b).map(v => Json.Obj(Chunk.single("Right" -> v))) @@ -428,12 +428,21 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { pad(indent, out) } - override final def toJsonAST(as: Array[A]): Either[String, Json] = - as.map(A.toJsonAST) - .foldLeft[Either[String, Chunk[Json]]](Right(Chunk.empty)) { (s, i) => - s.flatMap(chunk => i.map(item => chunk :+ item)) + override def toJsonAST(as: Array[A]): Either[String, Json] = { + val len = as.length + val buf = new Array[Json](len) + var i = 0 + while (i < len) { + A.toJsonAST(as(i)) match { + case Right(json) => + buf(i) = json + i += 1 + case left => + return left } - .map(Json.Arr(_)) + } + new Right(Json.Arr(Chunk.fromArray(buf))) + } } implicit def seq[A: JsonEncoder]: JsonEncoder[Seq[A]] = iterable[A, Seq] @@ -491,12 +500,22 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { pad(indent, out) } - override final def toJsonAST(as: List[A]): Either[String, Json] = - as.map(A.toJsonAST) - .foldLeft[Either[String, Chunk[Json]]](Right(Chunk.empty)) { (s, i) => - s.flatMap(chunk => i.map(item => chunk :+ item)) + override def toJsonAST(as: List[A]): Either[String, Json] = { + var as_ = as + val buf = new Array[Json](as_.size) + var i = 0 + while (as_ ne Nil) { + A.toJsonAST(as_.head) match { + case Right(json) => + as_ = as_.tail + buf(i) = json + i += 1 + case left => + return left } - .map(Json.Arr(_)) + } + new Right(Json.Arr(Chunk.fromArray(buf))) + } } implicit def vector[A: JsonEncoder]: JsonEncoder[Vector[A]] = iterable[A, Vector] @@ -564,12 +583,21 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { pad(indent, out) } - override final def toJsonAST(as: T[A]): Either[String, Json] = - as.map(A.toJsonAST) - .foldLeft[Either[String, Chunk[Json]]](Right(Chunk.empty)) { (s, i) => - s.flatMap(chunk => i.map(item => chunk :+ item)) + override def toJsonAST(as: T[A]): Either[String, Json] = { + val it = as.iterator + val buf = new Array[Json](as.size) + var i = 0 + while (it.hasNext) { + A.toJsonAST(it.next()) match { + case Right(json) => + buf(i) = json + i += 1 + case left => + return left } - .map(Json.Arr(_)) + } + new Right(Json.Arr(Chunk.fromArray(buf))) + } } // not implicit because this overlaps with encoders for lists of tuples @@ -621,16 +649,25 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { pad(indent, out) } - override final def toJsonAST(kvs: T[K, A]): Either[String, Json] = - kvs - .foldLeft[Either[String, Chunk[(String, Json)]]](Right(Chunk.empty)) { case (s, (k, v)) => - for { - chunk <- s - key = K.unsafeEncodeField(k) - value <- A.toJsonAST(v) - } yield if (value == Json.Null) chunk else chunk :+ (key -> value) + override def toJsonAST(kvs: T[K, A]): Either[String, Json] = { + val it = kvs.iterator + var buf = new Array[(String, Json)](kvs.size) + var i = 0 + while (it.hasNext) { + val kv = it.next() + A.toJsonAST(kv._2) match { + case Right(json) => + if (json ne Json.Null) { + buf(i) = (K.unsafeEncodeField(kv._1), json) + i += 1 + } + case left => + return left } - .map(Json.Obj(_)) + } + if (i != buf.length) buf = java.util.Arrays.copyOf(buf, i) + new Right(Json.Obj(Chunk.fromArray(buf))) + } } // not implicit because this overlaps with encoders for lists of tuples @@ -653,7 +690,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: DayOfWeek): Either[String, Json] = + override def toJsonAST(a: DayOfWeek): Either[String, Json] = new Right(new Json.Str(a.toString)) } @@ -664,7 +701,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: Duration): Either[String, Json] = + override def toJsonAST(a: Duration): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -675,7 +712,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: Instant): Either[String, Json] = + override def toJsonAST(a: Instant): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -686,7 +723,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: LocalDate): Either[String, Json] = + override def toJsonAST(a: LocalDate): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -697,7 +734,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: LocalDateTime): Either[String, Json] = + override def toJsonAST(a: LocalDateTime): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -708,7 +745,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: LocalTime): Either[String, Json] = + override def toJsonAST(a: LocalTime): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -719,7 +756,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: Month): Either[String, Json] = + override def toJsonAST(a: Month): Either[String, Json] = new Right(new Json.Str(a.toString)) } @@ -730,7 +767,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: MonthDay): Either[String, Json] = + override def toJsonAST(a: MonthDay): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -741,7 +778,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: OffsetDateTime): Either[String, Json] = + override def toJsonAST(a: OffsetDateTime): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -752,7 +789,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: OffsetTime): Either[String, Json] = + override def toJsonAST(a: OffsetTime): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -763,7 +800,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: Period): Either[String, Json] = + override def toJsonAST(a: Period): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -774,7 +811,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: Year): Either[String, Json] = + override def toJsonAST(a: Year): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -785,7 +822,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: YearMonth): Either[String, Json] = + override def toJsonAST(a: YearMonth): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -796,7 +833,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: ZonedDateTime): Either[String, Json] = + override def toJsonAST(a: ZonedDateTime): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -807,7 +844,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: ZoneId): Either[String, Json] = + override def toJsonAST(a: ZoneId): Either[String, Json] = new Right(new Json.Str(a.getId)) } @@ -818,7 +855,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: ZoneOffset): Either[String, Json] = + override def toJsonAST(a: ZoneOffset): Either[String, Json] = new Right(new Json.Str(serializers.toString(a))) } @@ -829,7 +866,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: UUID): Either[String, Json] = + override def toJsonAST(a: UUID): Either[String, Json] = new Right(new Json.Str(SafeNumbers.toString(a))) } @@ -840,7 +877,7 @@ private[json] trait EncoderLowPriority3 extends EncoderLowPriority4 { out.write('"') } - override final def toJsonAST(a: java.util.Currency): Either[String, Json] = + override def toJsonAST(a: java.util.Currency): Either[String, Json] = new Right(new Json.Str(a.toString)) } } From 5bd5dd338f4061787554ac1c46bd46eec2a6eaeb Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 27 Feb 2025 10:31:26 +0100 Subject: [PATCH 194/311] Yet more efficient encoding of sum types (#1345) --- .../src/main/scala-2.x/zio/json/macros.scala | 15 +++++++-------- .../shared/src/main/scala-3/zio/json/macros.scala | 15 +++++++-------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 9f0ae7e41..42d8feacc 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -541,8 +541,7 @@ object DeriveJsonEncoder { out.write(',') JsonEncoder.pad(indent_, out) } else prevFields = true - if (indent.isEmpty) out.write(field.encodedName) - else out.write(field.prettyEncodedName) + out.write(if (indent eq None) field.encodedName else field.prettyEncodedName) field.encoder.unsafeEncode(p, indent_, out) } } @@ -614,9 +613,9 @@ object DeriveJsonEncoder { val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) out.write(encodedNames(idx)) - if (indent.isEmpty) out.write(':') + if (indent eq None) out.write(':') else out.write(" : ") - tcs(idx).unsafeEncode(casts(idx)(a), indent_, out) + tcs(idx).unsafeEncode(a, indent_, out) JsonEncoder.pad(indent, out) out.write('}') } @@ -624,7 +623,7 @@ object DeriveJsonEncoder { override def toJsonAST(a: A): Either[String, Json] = { var idx = 0 while (!casts(idx).isDefinedAt(a)) idx += 1 - tcs(idx).toJsonAST(casts(idx)(a)).map(inner => new Json.Obj(Chunk(names(idx) -> inner))) + tcs(idx).toJsonAST(a).map(inner => new Json.Obj(Chunk(names(idx) -> inner))) } } } else { @@ -640,17 +639,17 @@ object DeriveJsonEncoder { val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) out.write(encodedHintFieldName) - if (indent.isEmpty) out.write(':') + if (indent eq None) out.write(':') else out.write(" : ") out.write(encodedNames(idx)) // whitespace is always off by 2 spaces at the end, probably not worth fixing - tcs(idx).unsafeEncode(casts(idx)(a), indent, new NestedWriter(out, indent_)) + tcs(idx).unsafeEncode(a, indent, new NestedWriter(out, indent_)) } override final def toJsonAST(a: A): Either[String, Json] = { var idx = 0 while (!casts(idx).isDefinedAt(a)) idx += 1 - tcs(idx).toJsonAST(casts(idx)(a)).flatMap { + tcs(idx).toJsonAST(a).flatMap { case o: Json.Obj => val hintField = hintFieldName -> new Json.Str(names(idx)) new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 8f9cd7a08..b44dc56b4 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -561,8 +561,7 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv out.write(',') JsonEncoder.pad(indent_, out) } else prevFields = true - if (indent.isEmpty) out.write(field.encodedName) - else out.write(field.prettyEncodedName) + out.write(if (indent eq None) field.encodedName else field.prettyEncodedName) field.encoder.unsafeEncode(p, indent_, out) } } @@ -636,9 +635,9 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) out.write(encodedNames(idx)) - if (indent.isEmpty) out.write(':') + if (indent eq None) out.write(':') else out.write(" : ") - tcs(idx).unsafeEncode(casts(idx)(a), indent_, out) + tcs(idx).unsafeEncode(a, indent_, out) JsonEncoder.pad(indent, out) out.write('}') } @@ -646,7 +645,7 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv override def toJsonAST(a: A): Either[String, Json] = { var idx = 0 while (!casts(idx).isDefinedAt(a)) idx += 1 - tcs(idx).toJsonAST(casts(idx)(a)).map(inner => new Json.Obj(Chunk(names(idx) -> inner))) + tcs(idx).toJsonAST(a).map(inner => new Json.Obj(Chunk(names(idx) -> inner))) } } } else { @@ -662,17 +661,17 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv val indent_ = JsonEncoder.bump(indent) JsonEncoder.pad(indent_, out) out.write(encodedHintFieldName) - if (indent.isEmpty) out.write(':') + if (indent eq None) out.write(':') else out.write(" : ") out.write(encodedNames(idx)) // whitespace is always off by 2 spaces at the end, probably not worth fixing - tcs(idx).unsafeEncode(casts(idx)(a), indent, new DeriveJsonEncoder.NestedWriter(out, indent_)) + tcs(idx).unsafeEncode(a, indent, new DeriveJsonEncoder.NestedWriter(out, indent_)) } override final def toJsonAST(a: A): Either[String, Json] = { var idx = 0 while (!casts(idx).isDefinedAt(a)) idx += 1 - tcs(idx).toJsonAST(casts(idx)(a)).flatMap { + tcs(idx).toJsonAST(a).flatMap { case o: Json.Obj => val hintField = hintFieldName -> new Json.Str(names(idx)) new Right(new Json.Obj(hintField +: o.fields)) // hint field is always first From 3877ea752d66c4a5dc4b7bcd527891f6a335af39 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 27 Feb 2025 14:58:51 +0100 Subject: [PATCH 195/311] Yet more efficient encoding of sum types (#1346) --- .../src/main/scala-2.x/zio/json/macros.scala | 88 +++++++++++++++++-- .../src/main/scala-3/zio/json/macros.scala | 84 ++++++++++++++++-- .../src/main/scala/zio/json/JsonEncoder.scala | 62 +++++-------- 3 files changed, 181 insertions(+), 53 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 42d8feacc..db0ef8d11 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -527,20 +527,18 @@ object DeriveJsonEncoder { def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { out.write('{') val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) - val fields = this.fields - var idx = 0 - var prevFields = false + val fields = this.fields + var idx = 0 + var comma = false while (idx < fields.length) { val field = fields(idx) idx += 1 val p = field.p.dereference(a) if (field.skip(p)) () else { - if (prevFields) { - out.write(',') - JsonEncoder.pad(indent_, out) - } else prevFields = true + if (comma) out.write(',') + else comma = true + JsonEncoder.pad(indent_, out) out.write(if (indent eq None) field.encodedName else field.prettyEncodedName) field.encoder.unsafeEncode(p, indent_, out) } @@ -745,6 +743,80 @@ private[this] final class NestedWriter(out: Write, indent: Option[Int]) extends i += 1 } } + + @inline override def write(c1: Char, c2: Char): Unit = + if (state == 0) out.write(c1, c2) + else { + nonZeroStateWrite(c1) + nonZeroStateWrite(c2) + } + + @inline override def write(c1: Char, c2: Char, c3: Char): Unit = + if (state == 0) out.write(c1, c2, c3) + else { + nonZeroStateWrite(c1) + nonZeroStateWrite(c2) + nonZeroStateWrite(c3) + } + + @inline override def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = + if (state == 0) out.write(c1, c2, c3, c4) + else { + nonZeroStateWrite(c1) + nonZeroStateWrite(c2) + nonZeroStateWrite(c3) + nonZeroStateWrite(c4) + } + + @inline override def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = + if (state == 0) out.write(c1, c2, c3, c4, c5) + else { + nonZeroStateWrite(c1) + nonZeroStateWrite(c2) + nonZeroStateWrite(c3) + nonZeroStateWrite(c4) + nonZeroStateWrite(c5) + } + + @inline override def write(s: Short): Unit = + if (state == 0) out.write(s) + else { + nonZeroStateWrite((s & 0xff).toChar) + nonZeroStateWrite((s >> 8).toChar) + } + + @inline override def write(s1: Short, s2: Short): Unit = + if (state == 0) out.write(s1, s2) + else { + nonZeroStateWrite((s1 & 0xff).toChar) + nonZeroStateWrite((s1 >> 8).toChar) + nonZeroStateWrite((s2 & 0xff).toChar) + nonZeroStateWrite((s2 >> 8).toChar) + } + + @inline override def write(s1: Short, s2: Short, s3: Short): Unit = + if (state == 0) out.write(s1, s2, s3) + else { + nonZeroStateWrite((s1 & 0xff).toChar) + nonZeroStateWrite((s1 >> 8).toChar) + nonZeroStateWrite((s2 & 0xff).toChar) + nonZeroStateWrite((s2 >> 8).toChar) + nonZeroStateWrite((s3 & 0xff).toChar) + nonZeroStateWrite((s3 >> 8).toChar) + } + + @inline override def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = + if (state == 0) out.write(s1, s2, s3, s4) + else { + nonZeroStateWrite((s1 & 0xff).toChar) + nonZeroStateWrite((s1 >> 8).toChar) + nonZeroStateWrite((s2 & 0xff).toChar) + nonZeroStateWrite((s2 >> 8).toChar) + nonZeroStateWrite((s3 & 0xff).toChar) + nonZeroStateWrite((s3 >> 8).toChar) + nonZeroStateWrite((s4 & 0xff).toChar) + nonZeroStateWrite((s4 >> 8).toChar) + } } object DeriveJsonCodec { diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index b44dc56b4..0aef5df14 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -547,20 +547,18 @@ sealed class JsonEncoderDerivation(config: JsonCodecConfiguration) extends Deriv def unsafeEncode(a: A, indent: Option[Int], out: Write): Unit = { out.write('{') val indent_ = JsonEncoder.bump(indent) - JsonEncoder.pad(indent_, out) val fields = this.fields var idx = 0 - var prevFields = false + var comma = false while (idx < fields.length) { val field = fields(idx) idx += 1 val p = field.p.deref(a) if (field.skip(p)) () else { - if (prevFields) { - out.write(',') - JsonEncoder.pad(indent_, out) - } else prevFields = true + if (comma) out.write(',') + else comma = true + JsonEncoder.pad(indent_, out) out.write(if (indent eq None) field.encodedName else field.prettyEncodedName) field.encoder.unsafeEncode(p, indent_, out) } @@ -766,6 +764,80 @@ object DeriveJsonEncoder extends JsonEncoderDerivation(JsonCodecConfiguration.de i += 1 } } + + @inline override def write(c1: Char, c2: Char): Unit = + if (state == 0) out.write(c1, c2) + else { + nonZeroStateWrite(c1) + nonZeroStateWrite(c2) + } + + @inline override def write(c1: Char, c2: Char, c3: Char): Unit = + if (state == 0) out.write(c1, c2, c3) + else { + nonZeroStateWrite(c1) + nonZeroStateWrite(c2) + nonZeroStateWrite(c3) + } + + @inline override def write(c1: Char, c2: Char, c3: Char, c4: Char): Unit = + if (state == 0) out.write(c1, c2, c3, c4) + else { + nonZeroStateWrite(c1) + nonZeroStateWrite(c2) + nonZeroStateWrite(c3) + nonZeroStateWrite(c4) + } + + @inline override def write(c1: Char, c2: Char, c3: Char, c4: Char, c5: Char): Unit = + if (state == 0) out.write(c1, c2, c3, c4, c5) + else { + nonZeroStateWrite(c1) + nonZeroStateWrite(c2) + nonZeroStateWrite(c3) + nonZeroStateWrite(c4) + nonZeroStateWrite(c5) + } + + @inline override def write(s: Short): Unit = + if (state == 0) out.write(s) + else { + nonZeroStateWrite((s & 0xff).toChar) + nonZeroStateWrite((s >> 8).toChar) + } + + @inline override def write(s1: Short, s2: Short): Unit = + if (state == 0) out.write(s1, s2) + else { + nonZeroStateWrite((s1 & 0xff).toChar) + nonZeroStateWrite((s1 >> 8).toChar) + nonZeroStateWrite((s2 & 0xff).toChar) + nonZeroStateWrite((s2 >> 8).toChar) + } + + @inline override def write(s1: Short, s2: Short, s3: Short): Unit = + if (state == 0) out.write(s1, s2, s3) + else { + nonZeroStateWrite((s1 & 0xff).toChar) + nonZeroStateWrite((s1 >> 8).toChar) + nonZeroStateWrite((s2 & 0xff).toChar) + nonZeroStateWrite((s2 >> 8).toChar) + nonZeroStateWrite((s3 & 0xff).toChar) + nonZeroStateWrite((s3 >> 8).toChar) + } + + @inline override def write(s1: Short, s2: Short, s3: Short, s4: Short): Unit = + if (state == 0) out.write(s1, s2, s3, s4) + else { + nonZeroStateWrite((s1 & 0xff).toChar) + nonZeroStateWrite((s1 >> 8).toChar) + nonZeroStateWrite((s2 & 0xff).toChar) + nonZeroStateWrite((s2 >> 8).toChar) + nonZeroStateWrite((s3 & 0xff).toChar) + nonZeroStateWrite((s3 >> 8).toChar) + nonZeroStateWrite((s4 & 0xff).toChar) + nonZeroStateWrite((s4 >> 8).toChar) + } } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 32d51f9a9..01d513f9f 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -414,14 +414,11 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { private[this] def unsafeEncodePadded(as: Array[A], indent: Option[Int], out: Write): Unit = { val indent_ = bump(indent) - pad(indent_, out) - val len = as.length - var i = 0 + val len = as.length + var i = 0 while (i < len) { - if (i != 0) { - out.write(',') - pad(indent_, out) - } + if (i != 0) out.write(',') + pad(indent_, out) A.unsafeEncode(as(i), indent_, out) i += 1 } @@ -473,11 +470,9 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { } private[this] def unsafeEncodeCompact(as: List[A], indent: Option[Int], out: Write): Unit = { - var as_ = as - var first = true + var as_ = as while (as_ ne Nil) { - if (first) first = false - else out.write(',') + if (as_ ne as) out.write(',') A.unsafeEncode(as_.head, indent, out) as_ = as_.tail } @@ -485,15 +480,10 @@ private[json] trait EncoderLowPriority1 extends EncoderLowPriority2 { private[this] def unsafeEncodePadded(as: List[A], indent: Option[Int], out: Write): Unit = { val indent_ = bump(indent) - pad(indent_, out) - var as_ = as - var first = true + var as_ = as while (as_ ne Nil) { - if (first) first = false - else { - out.write(',') - pad(indent_, out) - } + if (as_ ne as) out.write(',') + pad(indent_, out) A.unsafeEncode(as_.head, indent_, out) as_ = as_.tail } @@ -560,24 +550,21 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { private[this] def unsafeEncodeCompact(as: T[A], indent: Option[Int], out: Write): Unit = as.foreach { - var first = true + var comma = false a => - if (first) first = false - else out.write(',') + if (comma) out.write(',') + else comma = true A.unsafeEncode(a, indent, out) } private[this] def unsafeEncodePadded(as: T[A], indent: Option[Int], out: Write): Unit = { val indent_ = bump(indent) - pad(indent_, out) as.foreach { - var first = true + var comma = false a => - if (first) first = false - else { - out.write(',') - pad(indent_, out) - } + if (comma) out.write(',') + else comma = true + pad(indent_, out) A.unsafeEncode(a, indent_, out) } pad(indent, out) @@ -618,11 +605,11 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { private[this] def unsafeEncodeCompact(kvs: T[K, A], indent: Option[Int], out: Write): Unit = kvs.foreach { - var first = true + var comma = false kv => if (!A.isNothing(kv._2)) { - if (first) first = false - else out.write(',') + if (comma) out.write(',') + else comma = true string.unsafeEncode(K.unsafeEncodeField(kv._1), indent, out) out.write(':') A.unsafeEncode(kv._2, indent, out) @@ -631,16 +618,13 @@ private[json] trait EncoderLowPriority2 extends EncoderLowPriority3 { private[this] def unsafeEncodePadded(kvs: T[K, A], indent: Option[Int], out: Write): Unit = { val indent_ = bump(indent) - pad(indent_, out) kvs.foreach { - var first = true + var comman = false kv => if (!A.isNothing(kv._2)) { - if (first) first = false - else { - out.write(',') - pad(indent_, out) - } + if (comman) out.write(',') + else comman = true + pad(indent_, out) string.unsafeEncode(K.unsafeEncodeField(kv._1), indent_, out) out.write(" : ") A.unsafeEncode(kv._2, indent_, out) From d179bf3abfb63dda2b2dd3397778b4dbb111ea41 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 28 Feb 2025 11:38:59 +0100 Subject: [PATCH 196/311] An example of a custom codec for union of standard types using an internal API (#1349) --- .../zio/json/CodecVersionSpecificSpec.scala | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala index d0e7a1091..ab76fe5bc 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/CodecVersionSpecificSpec.scala @@ -38,6 +38,50 @@ object CodecVersionSpecificSpec extends ZIOSpecDefault { case class Foo(aOrB: "A" | "B", optA: Option["A"]) derives JsonCodec assertTrue(Foo("A", Some("A")).toJson.fromJson[Foo] == Right(Foo("A", Some("A")))) + }, + test("Custom codec for union of standard types using an internal API") { + import zio.json.internal._ + + type Value = Null | String | Int | Boolean + + final case class MyDomain(v: Value) + + object MyDomain: + given JsonCodec[MyDomain] = new JsonCodec[MyDomain]( + (a: MyDomain, indent: Option[Int], out: Write) => + a.v match { + case i: Int => SafeNumbers.write(i, out) + case b: Boolean => out.write(if (b) "true" else "false") + case s: String => JsonEncoder.string.unsafeEncode(s, indent, out) + case null => out.write("null") + }, + (trace: List[JsonError], in: RetractReader) => + new MyDomain({ + val c = in.nextNonWhitespace() + if (c == '"') { + in.retract() + Lexer.string(trace, in).toString + } else if (c == 't' && in.readChar() == 'r' && in.readChar() == 'u' && in.readChar() == 'e') { + true + } else if ( + c == 'f' && in.readChar() == 'a' && in.readChar() == 'l' && in.readChar() == 's' && in + .readChar() == 'e' + ) { + false + } else if (c == 'n' && in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') { + null + } else { + in.retract() + Lexer.int(trace, in) + } + }) + ) + + assertTrue( + List(MyDomain("xxx"), MyDomain(777), MyDomain(true), MyDomain(false), MyDomain(null)).toJson + .fromJson[List[MyDomain]] == + Right(List(MyDomain("xxx"), MyDomain(777), MyDomain(true), MyDomain(false), MyDomain(null))) + ) } ) } From 921a5978d2179e28c87d3cc26919c539e4fa024c Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 28 Feb 2025 11:39:16 +0100 Subject: [PATCH 197/311] Update zio, zio-streams, zio-test, ... to 2.1.16 (#1347) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b454a1d81..ef0805861 100644 --- a/build.sbt +++ b/build.sbt @@ -58,7 +58,7 @@ addCommandAlias( "zioJsonMacrosNative/test" ) -val zioVersion = "2.1.15" +val zioVersion = "2.1.16" lazy val zioJsonRoot = project .in(file(".")) From 6f9e29d62ed09cf717f6a636a8a965067a75933b Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Fri, 28 Feb 2025 16:12:43 +0100 Subject: [PATCH 198/311] Fix error message inconsistencies between immediate decoding and through AST (#1350) --- .../src/main/scala/zio/json/JsonDecoder.scala | 157 +++++++++++++----- .../main/scala/zio/json/internal/lexer.scala | 153 ++++++++--------- .../src/test/scala/zio/json/DecoderSpec.scala | 22 +++ 3 files changed, 211 insertions(+), 121 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 146712994..9b862983e 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -16,6 +16,7 @@ package zio.json import zio.json.ast.Json +import zio.json.internal.Lexer.NumberMaxBits import zio.json.internal._ import zio.json.javatime.parsers import zio.json.uuid.UUIDParser @@ -310,16 +311,22 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Byte = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Byte = { json match { case n: Json.Num => - try n.value.byteValueExact + try return n.value.byteValueExact catch { - case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + case _: ArithmeticException => } - case s: Json.Str => Lexer.byte(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case s: Json.Str => + try return Lexer.byte(trace, new FastStringReader(s.value)) + catch { + case _: UnexpectedEnd => + } + case _ => } + Lexer.error("expected a Byte", trace) + } } implicit val short: JsonDecoder[Short] = new JsonDecoder[Short] { @@ -334,16 +341,22 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Short = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Short = { json match { case n: Json.Num => - try n.value.shortValueExact + try return n.value.shortValueExact catch { - case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + case _: ArithmeticException => } - case s: Json.Str => Lexer.short(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case s: Json.Str => + try return Lexer.short(trace, new FastStringReader(s.value)) + catch { + case _: UnexpectedEnd => + } + case _ => } + Lexer.error("expected a Short", trace) + } } implicit val int: JsonDecoder[Int] = new JsonDecoder[Int] { @@ -358,16 +371,22 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Int = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Int = { json match { case n: Json.Num => - try n.value.intValueExact + try return n.value.intValueExact + catch { + case _: ArithmeticException => + } + case s: Json.Str => + try return Lexer.int(trace, new FastStringReader(s.value)) catch { - case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + case _: UnexpectedEnd => } - case s: Json.Str => Lexer.int(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case _ => } + Lexer.error("expected a Int", trace) + } } implicit val long: JsonDecoder[Long] = new JsonDecoder[Long] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Long = @@ -381,16 +400,22 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Long = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Long = { json match { case n: Json.Num => - try n.value.longValueExact + try return n.value.longValueExact + catch { + case _: ArithmeticException => + } + case s: Json.Str => + try return Lexer.long(trace, new FastStringReader(s.value)) catch { - case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + case _: UnexpectedEnd => } - case s: Json.Str => Lexer.long(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case _ => } + Lexer.error("expected a Long", trace) + } } implicit val bigInteger: JsonDecoder[java.math.BigInteger] = new JsonDecoder[java.math.BigInteger] { @@ -405,16 +430,22 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigInteger = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigInteger = { json match { case n: Json.Num => - try n.value.toBigIntegerExact + try return n.value.toBigIntegerExact catch { - case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + case _: ArithmeticException => } - case s: Json.Str => Lexer.bigInteger(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case s: Json.Str => + try return Lexer.bigInteger(trace, new FastStringReader(s.value)) + catch { + case _: UnexpectedEnd => + } + case _ => } + Lexer.error(s"expected a $NumberMaxBits-bit BigInteger", trace) + } } implicit val scalaBigInt: JsonDecoder[BigInt] = new JsonDecoder[BigInt] { def unsafeDecode(trace: List[JsonError], in: RetractReader): BigInt = @@ -428,16 +459,22 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigInt = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigInt = { json match { case n: Json.Num => - try BigInt(n.value.toBigIntegerExact) + try return BigInt(n.value.toBigIntegerExact) + catch { + case _: ArithmeticException => + } + case s: Json.Str => + try return Lexer.bigInt(trace, new FastStringReader(s.value)) catch { - case ex: ArithmeticException => Lexer.error(ex.getMessage, trace) + case _: UnexpectedEnd => } - case s: Json.Str => Lexer.bigInt(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case _ => } + Lexer.error(s"expected a $NumberMaxBits-bit BigInt", trace) + } } implicit val float: JsonDecoder[Float] = new JsonDecoder[Float] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Float = @@ -451,12 +488,19 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Float = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Float = { json match { - case n: Json.Num => n.value.floatValue - case s: Json.Str => Lexer.float(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case n: Json.Num => + return n.value.floatValue + case s: Json.Str => + try return Lexer.float(trace, new FastStringReader(s.value)) + catch { + case _: UnexpectedEnd => + } + case _ => } + Lexer.error("expected a Float", trace) + } } implicit val double: JsonDecoder[Double] = new JsonDecoder[Double] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Double = @@ -470,12 +514,19 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Double = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Double = { json match { - case n: Json.Num => n.value.doubleValue - case s: Json.Str => Lexer.double(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case n: Json.Num => + return n.value.doubleValue + case s: Json.Str => + try return Lexer.double(trace, new FastStringReader(s.value)) + catch { + case _: UnexpectedEnd => + } + case _ => } + Lexer.error("expected a Double", trace) + } } implicit val bigDecimal: JsonDecoder[java.math.BigDecimal] = new JsonDecoder[java.math.BigDecimal] { def unsafeDecode(trace: List[JsonError], in: RetractReader): java.math.BigDecimal = @@ -489,12 +540,19 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigDecimal = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.math.BigDecimal = { json match { - case n: Json.Num => n.value - case s: Json.Str => Lexer.bigDecimal(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case n: Json.Num => + return n.value + case s: Json.Str => + try return Lexer.bigDecimal(trace, new FastStringReader(s.value)) + catch { + case _: UnexpectedEnd => + } + case _ => } + Lexer.error(s"expected a BigDecimal with $NumberMaxBits-bit mantissa", trace) + } } implicit val scalaBigDecimal: JsonDecoder[BigDecimal] = new JsonDecoder[BigDecimal] { def unsafeDecode(trace: List[JsonError], in: RetractReader): BigDecimal = @@ -508,12 +566,19 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with a } - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigDecimal = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigDecimal = { json match { - case n: Json.Num => new BigDecimal(n.value, BigDecimal.defaultMathContext) - case s: Json.Str => Lexer.bigDecimal(trace, new FastStringReader(s.value)) - case _ => Lexer.error("expected number", trace) + case n: Json.Num => + return new BigDecimal(n.value) + case s: Json.Str => + try return new BigDecimal(Lexer.bigDecimal(trace, new FastStringReader(s.value))) + catch { + case _: UnexpectedEnd => + } + case _ => } + Lexer.error(s"expected a BigDecimal with $NumberMaxBits-bit mantissa", trace) + } } // Option treats empty and null values as Nothing and passes values to the decoder. // diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index a504c9096..e242c4320 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -188,88 +188,91 @@ object Lexer { def string(trace: List[JsonError], in: OneCharReader): CharSequence = { var c = in.nextNonWhitespace() - if (c != '"') error("'\"'", c, trace) - var cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - else if (c < ' ') error("invalid control in string", trace) - if (i == cs.length) cs = java.util.Arrays.copyOf(cs, i << 1) - cs(i) = c - i += 1 + if (c == '"') { + var cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + else if (c < ' ') error("invalid control in string", trace) + if (i == cs.length) cs = java.util.Arrays.copyOf(cs, i << 1) + cs(i) = c + i += 1 + } + return new String(cs, 0, i) } - new String(cs, 0, i) + error("expected string", trace) } def uuid(trace: List[JsonError], in: OneCharReader): UUID = { var c = in.nextNonWhitespace() - if (c != '"') error("'\"'", c, trace) - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - if (i == 36 || c > 0xff) uuidError(trace) - cs(i) = c - i += 1 - } - if ( - i == 36 && { - val c1 = cs(8) - val c2 = cs(13) - val c3 = cs(18) - val c4 = cs(23) - c1 == '-' && c2 == '-' && c3 == '-' && c4 == '-' + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + if (i == 36 || c > 0xff) uuidError(trace) + cs(i) = c + i += 1 } - ) { - val ds = hexDigits - val msb1 = - ds(cs(0).toInt).toLong << 28 | - (ds(cs(1).toInt) << 24 | - ds(cs(2).toInt) << 20 | - ds(cs(3).toInt) << 16 | - ds(cs(4).toInt) << 12 | - ds(cs(5).toInt) << 8 | - ds(cs(6).toInt) << 4 | - ds(cs(7).toInt)) - val msb2 = - (ds(cs(9).toInt) << 12 | - ds(cs(10).toInt) << 8 | - ds(cs(11).toInt) << 4 | - ds(cs(12).toInt)).toLong - val msb3 = - (ds(cs(14).toInt) << 12 | - ds(cs(15).toInt) << 8 | - ds(cs(16).toInt) << 4 | - ds(cs(17).toInt)).toLong - val lsb1 = - (ds(cs(19).toInt) << 12 | - ds(cs(20).toInt) << 8 | - ds(cs(21).toInt) << 4 | - ds(cs(22).toInt)).toLong - val lsb2 = - (ds(cs(24).toInt) << 16 | - ds(cs(25).toInt) << 12 | - ds(cs(26).toInt) << 8 | - ds(cs(27).toInt) << 4 | - ds(cs(28).toInt)).toLong << 28 | - (ds(cs(29).toInt) << 24 | - ds(cs(30).toInt) << 20 | - ds(cs(31).toInt) << 16 | - ds(cs(32).toInt) << 12 | - ds(cs(33).toInt) << 8 | - ds(cs(34).toInt) << 4 | - ds(cs(35).toInt)) - if ((msb1 | msb2 | msb3 | lsb1 | lsb2) >= 0L) { - return new UUID(msb1 << 32 | msb2 << 16 | msb3, lsb1 << 48 | lsb2) + if ( + i == 36 && { + val c1 = cs(8) + val c2 = cs(13) + val c3 = cs(18) + val c4 = cs(23) + c1 == '-' && c2 == '-' && c3 == '-' && c4 == '-' + } + ) { + val ds = hexDigits + val msb1 = + ds(cs(0).toInt).toLong << 28 | + (ds(cs(1).toInt) << 24 | + ds(cs(2).toInt) << 20 | + ds(cs(3).toInt) << 16 | + ds(cs(4).toInt) << 12 | + ds(cs(5).toInt) << 8 | + ds(cs(6).toInt) << 4 | + ds(cs(7).toInt)) + val msb2 = + (ds(cs(9).toInt) << 12 | + ds(cs(10).toInt) << 8 | + ds(cs(11).toInt) << 4 | + ds(cs(12).toInt)).toLong + val msb3 = + (ds(cs(14).toInt) << 12 | + ds(cs(15).toInt) << 8 | + ds(cs(16).toInt) << 4 | + ds(cs(17).toInt)).toLong + val lsb1 = + (ds(cs(19).toInt) << 12 | + ds(cs(20).toInt) << 8 | + ds(cs(21).toInt) << 4 | + ds(cs(22).toInt)).toLong + val lsb2 = + (ds(cs(24).toInt) << 16 | + ds(cs(25).toInt) << 12 | + ds(cs(26).toInt) << 8 | + ds(cs(27).toInt) << 4 | + ds(cs(28).toInt)).toLong << 28 | + (ds(cs(29).toInt) << 24 | + ds(cs(30).toInt) << 20 | + ds(cs(31).toInt) << 16 | + ds(cs(32).toInt) << 12 | + ds(cs(33).toInt) << 8 | + ds(cs(34).toInt) << 4 | + ds(cs(35).toInt)) + if ((msb1 | msb2 | msb3 | lsb1 | lsb2) >= 0L) { + return new UUID(msb1 << 32 | msb2 << 16 | msb3, lsb1 << 48 | lsb2) + } + } else if (i <= 36) { + return uuidExtended(trace, cs, i) } - } else if (i <= 36) { - return uuidExtended(trace, cs, i) } uuidError(trace) } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 56dcd4aa3..d7a2ff493 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -673,6 +673,22 @@ object DecoderSpec extends ZIOSpecDefault { isLeft(equalTo(".is[0].str(expected string)")) ) }, + test("errors are consistent with direct decoding") { + assert("""{}""".fromJson[Message])(isLeft(equalTo(".v1(missing)"))) && + assert("""{}""".fromJson[Json].flatMap(_.as[Message]))(isLeft(equalTo(".v1(missing)"))) && + assert("""{"v1":"","v2":""}""".fromJson[Message])( + isLeft(equalTo(".v1(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert("""{"v1":"","v2":""}""".fromJson[Json].flatMap(_.as[Message]))( + isLeft(equalTo(".v1(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert("""{"v1":1,"v2":1}""".fromJson[Message])( + isLeft(equalTo(".v2(expected string)")) + ) && + assert("""{"v1":1,"v2":1}""".fromJson[Json].flatMap(_.as[Message]))( + isLeft(equalTo(".v2(expected string)")) + ) + }, test("default field value") { import exampleproducts._ @@ -1008,4 +1024,10 @@ object DecoderSpec extends ZIOSpecDefault { implicitly[JsonFieldEncoder[PersonId]] implicitly[JsonFieldDecoder[PersonId]] } + + case class Message(v1: math.BigDecimal, v2: String) + + object Message { + implicit val decoder: JsonDecoder[Message] = DeriveJsonDecoder.gen[Message] + } } From c39844f93c65e0875d1cafb65f855bac18124b4c Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 2 Mar 2025 07:39:52 +0100 Subject: [PATCH 199/311] Update scalafmt-core to 3.9.2 (#1351) --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 70063b384..e7cfc5922 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.9.1" +version = "3.9.2" runner.dialect = scala213 maxColumn = 120 align.preset = most From 93113303a70d454ec8ad118a275de7afb5683096 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 3 Mar 2025 13:40:55 +0100 Subject: [PATCH 200/311] Fix missing error when decoding of `Json.Str` values that ends by non-digit characters to numbers (#1352) --- .../src/main/scala/zio/json/JsonDecoder.scala | 46 +-- .../src/test/scala/zio/json/DecoderSpec.scala | 296 +++++++++++++++--- 2 files changed, 284 insertions(+), 58 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 9b862983e..44c7b5c11 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -319,9 +319,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case _: ArithmeticException => } case s: Json.Str => - try return Lexer.byte(trace, new FastStringReader(s.value)) + try return UnsafeNumbers.byte_(new FastStringReader(s.value), true) catch { - case _: UnexpectedEnd => + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } @@ -349,9 +349,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case _: ArithmeticException => } case s: Json.Str => - try return Lexer.short(trace, new FastStringReader(s.value)) + try return UnsafeNumbers.short_(new FastStringReader(s.value), true) catch { - case _: UnexpectedEnd => + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } @@ -379,9 +379,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case _: ArithmeticException => } case s: Json.Str => - try return Lexer.int(trace, new FastStringReader(s.value)) + try return UnsafeNumbers.int_(new FastStringReader(s.value), true) catch { - case _: UnexpectedEnd => + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } @@ -408,9 +408,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case _: ArithmeticException => } case s: Json.Str => - try return Lexer.long(trace, new FastStringReader(s.value)) + try return UnsafeNumbers.long_(new FastStringReader(s.value), true) catch { - case _: UnexpectedEnd => + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } @@ -438,9 +438,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case _: ArithmeticException => } case s: Json.Str => - try return Lexer.bigInteger(trace, new FastStringReader(s.value)) + try return UnsafeNumbers.bigInteger_(new FastStringReader(s.value), true, Lexer.NumberMaxBits) catch { - case _: UnexpectedEnd => + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } @@ -467,9 +467,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case _: ArithmeticException => } case s: Json.Str => - try return Lexer.bigInt(trace, new FastStringReader(s.value)) + try return UnsafeNumbers.bigInt_(new FastStringReader(s.value), true, Lexer.NumberMaxBits) catch { - case _: UnexpectedEnd => + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } @@ -493,9 +493,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case n: Json.Num => return n.value.floatValue case s: Json.Str => - try return Lexer.float(trace, new FastStringReader(s.value)) + try return UnsafeNumbers.float_(new FastStringReader(s.value), true, Lexer.NumberMaxBits) catch { - case _: UnexpectedEnd => + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } @@ -519,9 +519,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case n: Json.Num => return n.value.doubleValue case s: Json.Str => - try return Lexer.double(trace, new FastStringReader(s.value)) + try return UnsafeNumbers.double_(new FastStringReader(s.value), true, Lexer.NumberMaxBits) catch { - case _: UnexpectedEnd => + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } @@ -545,9 +545,9 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with case n: Json.Num => return n.value case s: Json.Str => - try return Lexer.bigDecimal(trace, new FastStringReader(s.value)) + try return UnsafeNumbers.bigDecimal_(new FastStringReader(s.value), true, Lexer.NumberMaxBits) catch { - case _: UnexpectedEnd => + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } @@ -569,11 +569,13 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with override def unsafeFromJsonAST(trace: List[JsonError], json: Json): BigDecimal = { json match { case n: Json.Num => - return new BigDecimal(n.value) + return new BigDecimal(n.value, BigDecimal.defaultMathContext) case s: Json.Str => - try return new BigDecimal(Lexer.bigDecimal(trace, new FastStringReader(s.value))) - catch { - case _: UnexpectedEnd => + try { + val bd = UnsafeNumbers.bigDecimal_(new FastStringReader(s.value), true, Lexer.NumberMaxBits) + return new BigDecimal(bd, BigDecimal.defaultMathContext) + } catch { + case _: UnexpectedEnd | UnsafeNumbers.UnsafeNumber => } case _ => } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index d7a2ff493..7d04b4b43 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -12,7 +12,6 @@ import java.util.UUID import scala.collection.{ SortedMap, immutable, mutable } object DecoderSpec extends ZIOSpecDefault { - val spec: Spec[Environment, Any] = suite("Decoder")( suite("fromJson")( @@ -36,8 +35,8 @@ object DecoderSpec extends ZIOSpecDefault { assert("\"\\u0000\"".replace('0', 'g').fromJson[Char])(isLeft(equalTo("""(invalid charcode in string)"""))) }, test("byte") { - assert("-123".fromJson[Byte])(isRight(equalTo(-123: Byte))) && - assert("123".fromJson[Byte])(isRight(equalTo(123: Byte))) && + assert("-128".fromJson[Byte])(isRight(equalTo(Byte.MinValue))) && + assert("127".fromJson[Byte])(isRight(equalTo(Byte.MaxValue))) && assert("\"-123\"".fromJson[Byte])(isRight(equalTo(-123: Byte))) && assert("\"123\"".fromJson[Byte])(isRight(equalTo(123: Byte))) && assertTrue("+123".fromJson[Byte].isLeft) && @@ -47,8 +46,8 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue("\"NaN\"".fromJson[Byte].isLeft) }, test("short") { - assert("-12345".fromJson[Short])(isRight(equalTo(-12345: Short))) && - assert("12345".fromJson[Short])(isRight(equalTo(12345: Short))) && + assert("-32768".fromJson[Short])(isRight(equalTo(Short.MinValue))) && + assert("32767".fromJson[Short])(isRight(equalTo(Short.MaxValue))) && assert("\"-12345\"".fromJson[Short])(isRight(equalTo(-12345: Short))) && assert("\"12345\"".fromJson[Short])(isRight(equalTo(12345: Short))) && assertTrue("+12345".fromJson[Short].isLeft) && @@ -58,8 +57,8 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue("\"NaN\"".fromJson[Short].isLeft) }, test("int") { - assert("-1234567890".fromJson[Int])(isRight(equalTo(-1234567890))) && - assert("1234567890".fromJson[Int])(isRight(equalTo(1234567890))) && + assert("-2147483648".fromJson[Int])(isRight(equalTo(Int.MinValue))) && + assert("2147483647".fromJson[Int])(isRight(equalTo(Int.MaxValue))) && assert("\"-1234567890\"".fromJson[Int])(isRight(equalTo(-1234567890))) && assert("\"1234567890\"".fromJson[Int])(isRight(equalTo(1234567890))) && assertTrue("+1234567890".fromJson[Int].isLeft) && @@ -69,8 +68,8 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue("\"NaN\"".fromJson[Int].isLeft) }, test("long") { - assert("-123456789012345678".fromJson[Long])(isRight(equalTo(-123456789012345678L))) && - assert("123456789012345678".fromJson[Long])(isRight(equalTo(123456789012345678L))) && + assert("-9223372036854775808".fromJson[Long])(isRight(equalTo(Long.MinValue))) && + assert("9223372036854775807".fromJson[Long])(isRight(equalTo(Long.MaxValue))) && assert("\"-123456789012345678\"".fromJson[Long])(isRight(equalTo(-123456789012345678L))) && assert("\"123456789012345678\"".fromJson[Long])(isRight(equalTo(123456789012345678L))) && assertTrue("+123456789012345678".fromJson[Long].isLeft) && @@ -133,21 +132,11 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue("\"Infinity\"".fromJson[BigDecimal].isLeft) && assertTrue("\"+Infinity\"".fromJson[BigDecimal].isLeft) && assertTrue("\"-Infinity\"".fromJson[BigDecimal].isLeft) && - assertTrue("\"NaN\"".fromJson[BigDecimal].isLeft) - }, - test("BigDecimal from JSON AST") { - assert("13.38885989999999992505763657391071319580078125".fromJson[Json])( - isRight(equalTo(Json.Num(BigDecimal("13.38885989999999992505763657391071319580078125")))) - ) - }, - test("BigDecimal too large") { - // this big integer consumes more than 256 bits + assertTrue("\"NaN\"".fromJson[BigDecimal].isLeft) && assert( "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" .fromJson[BigDecimal] - )(isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)"))) - }, - test("BigDecimal exponent too large") { + )(isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)"))) && assert("1.23456789012345678901e-2147483648".fromJson[BigDecimal])( isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) ) && @@ -158,6 +147,29 @@ object DecoderSpec extends ZIOSpecDefault { isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) ) }, + test("java.math.BigDecimal") { + assert("-123.0e123".fromJson[java.math.BigDecimal])( + isRight(equalTo(new java.math.BigDecimal("-123.0e123"))) + ) && + assert("123.0e123".fromJson[java.math.BigDecimal])(isRight(equalTo(new java.math.BigDecimal("123.0e123")))) && + assertTrue("\"Infinity\"".fromJson[java.math.BigDecimal].isLeft) && + assertTrue("\"+Infinity\"".fromJson[java.math.BigDecimal].isLeft) && + assertTrue("\"-Infinity\"".fromJson[java.math.BigDecimal].isLeft) && + assertTrue("\"NaN\"".fromJson[java.math.BigDecimal].isLeft) && + assert( + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" + .fromJson[java.math.BigDecimal] + )(isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)"))) && + assert("1.23456789012345678901e-2147483648".fromJson[java.math.BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert("12345678901234567890.1e+2147483647".fromJson[java.math.BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert("123456789012345678901e+2147483647".fromJson[java.math.BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) + }, test("BigInteger") { assert("170141183460469231731687303715884105728".fromJson[BigInteger])( isRight(equalTo(new BigInteger("170141183460469231731687303715884105728"))) @@ -168,9 +180,7 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue("\"Infinity\"".fromJson[BigInteger].isLeft) && assertTrue("\"+Infinity\"".fromJson[BigInteger].isLeft) && assertTrue("\"-Infinity\"".fromJson[BigInteger].isLeft) && - assertTrue("\"NaN\"".fromJson[BigInteger].isLeft) - }, - test("BigInteger too large") { + assertTrue("\"NaN\"".fromJson[BigInteger].isLeft) && assert( "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851316546851" .fromJson[BigInteger] @@ -189,9 +199,7 @@ object DecoderSpec extends ZIOSpecDefault { assertTrue("\"Infinity\"".fromJson[BigInt].isLeft) && assertTrue("\"+Infinity\"".fromJson[BigInt].isLeft) && assertTrue("\"-Infinity\"".fromJson[BigInt].isLeft) && - assertTrue("\"NaN\"".fromJson[BigInt].isLeft) - }, - test("BigInt too large") { + assertTrue("\"NaN\"".fromJson[BigInt].isLeft) && assert( "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851316546851" .fromJson[BigInt] @@ -624,9 +632,6 @@ object DecoderSpec extends ZIOSpecDefault { } ), suite("fromJsonAST")( - test("BigDecimal") { - assert(Json.Num(123).as[BigDecimal])(isRight(equalTo(BigDecimal(123)))) - }, test("boolean") { assert(Json.Bool(true).as[Boolean])(isRight(equalTo(true))) && assert(Json.Str("true").as[Boolean])(isLeft(equalTo("(expected boolean)"))) @@ -640,20 +645,239 @@ object DecoderSpec extends ZIOSpecDefault { assert(Json.Str("xxx").as[Char])(isLeft(equalTo("(expected single character string)"))) && assert(Json.Bool(true).as[Char])(isLeft(equalTo("(expected single character string)"))) }, + test("byte") { + assert(Json.Num(Byte.MinValue).as[Byte])(isRight(equalTo(Byte.MinValue))) && + assert(Json.Num(Byte.MaxValue).as[Byte])(isRight(equalTo(Byte.MaxValue))) && + assert(Json.Str(Byte.MinValue.toString).as[Byte])(isRight(equalTo(Byte.MinValue))) && + assert(Json.Str(Byte.MaxValue.toString).as[Byte])(isRight(equalTo(Byte.MaxValue))) && + assertTrue(Json.Num(Byte.MinValue.toInt - 1).as[Byte].isLeft) && + assertTrue(Json.Num(Byte.MaxValue.toInt + 1).as[Byte].isLeft) && + assertTrue(Json.Str((Byte.MinValue.toInt - 1).toString).as[Byte].isLeft) && + assertTrue(Json.Str((Byte.MaxValue.toInt + 1).toString).as[Byte].isLeft) && + assertTrue(Json.Str("\"-123\"").as[Byte].isLeft) && + assertTrue(Json.Str("\"123\"").as[Byte].isLeft) && + assertTrue(Json.Str("123abc").as[Byte].isLeft) && + assertTrue(Json.Str("+123").as[Byte].isLeft) && + assertTrue(Json.Str("Infinity").as[Byte].isLeft) && + assertTrue(Json.Str("+Infinity").as[Byte].isLeft) && + assertTrue(Json.Str("-Infinity").as[Byte].isLeft) && + assertTrue(Json.Str("NaN").as[Byte].isLeft) + }, + test("short") { + assert(Json.Num(Short.MinValue).as[Short])(isRight(equalTo(Short.MinValue))) && + assert(Json.Num(Short.MaxValue).as[Short])(isRight(equalTo(Short.MaxValue))) && + assert(Json.Str(Short.MinValue.toString).as[Short])(isRight(equalTo(Short.MinValue))) && + assert(Json.Str(Short.MaxValue.toString).as[Short])(isRight(equalTo(Short.MaxValue))) && + assertTrue(Json.Num(Short.MinValue.toInt - 1).as[Short].isLeft) && + assertTrue(Json.Num(Short.MaxValue.toInt + 1).as[Short].isLeft) && + assertTrue(Json.Str((Short.MinValue.toInt - 1).toString).as[Short].isLeft) && + assertTrue(Json.Str((Short.MaxValue.toInt + 1).toString).as[Short].isLeft) && + assertTrue(Json.Str("\"-12345\"").as[Short].isLeft) && + assertTrue(Json.Str("\"12345\"").as[Short].isLeft) && + assertTrue(Json.Str("12345abc").as[Short].isLeft) && + assertTrue(Json.Str("+12345").as[Short].isLeft) && + assertTrue(Json.Str("Infinity").as[Short].isLeft) && + assertTrue(Json.Str("+Infinity").as[Short].isLeft) && + assertTrue(Json.Str("-Infinity").as[Short].isLeft) && + assertTrue(Json.Str("NaN").as[Short].isLeft) + }, + test("int") { + assert(Json.Num(Int.MinValue).as[Int])(isRight(equalTo(Int.MinValue))) && + assert(Json.Num(Int.MaxValue).as[Int])(isRight(equalTo(Int.MaxValue))) && + assert(Json.Str(Int.MinValue.toString).as[Int])(isRight(equalTo(Int.MinValue))) && + assert(Json.Str(Int.MaxValue.toString).as[Int])(isRight(equalTo(Int.MaxValue))) && + assertTrue(Json.Num(Int.MinValue.toLong - 1).as[Int].isLeft) && + assertTrue(Json.Num(Int.MaxValue.toLong + 1).as[Int].isLeft) && + assertTrue(Json.Str((Int.MinValue.toLong - 1).toString).as[Int].isLeft) && + assertTrue(Json.Str((Int.MaxValue.toLong + 1).toString).as[Int].isLeft) && + assertTrue(Json.Str("\"-1234567890\"").as[Int].isLeft) && + assertTrue(Json.Str("\"1234567890\"").as[Int].isLeft) && + assertTrue(Json.Str("1234567890abc").as[Int].isLeft) && + assertTrue(Json.Str("+1234567890").as[Int].isLeft) && + assertTrue(Json.Str("Infinity").as[Int].isLeft) && + assertTrue(Json.Str("+Infinity").as[Int].isLeft) && + assertTrue(Json.Str("-Infinity").as[Int].isLeft) && + assertTrue(Json.Str("NaN").as[Int].isLeft) + }, + test("long") { + assert(Json.Num(Long.MinValue).as[Long])(isRight(equalTo(Long.MinValue))) && + assert(Json.Num(Long.MaxValue).as[Long])(isRight(equalTo(Long.MaxValue))) && + assert(Json.Str(Long.MinValue.toString).as[Long])(isRight(equalTo(Long.MinValue))) && + assert(Json.Str(Long.MaxValue.toString).as[Long])(isRight(equalTo(Long.MaxValue))) && + assertTrue(Json.Num(BigDecimal(Long.MinValue) - 1).as[Long].isLeft) && + assertTrue(Json.Num(BigDecimal(Long.MaxValue) + 1).as[Long].isLeft) && + assertTrue(Json.Str((BigDecimal(Long.MinValue) - 1).toString).as[Long].isLeft) && + assertTrue(Json.Str((BigDecimal(Long.MaxValue) + 1).toString).as[Long].isLeft) && + assertTrue(Json.Str("\"-123456789012345678\"").as[Long].isLeft) && + assertTrue(Json.Str("\"123456789012345678\"").as[Long].isLeft) && + assertTrue(Json.Str("123456789012345678abc").as[Long].isLeft) && + assertTrue(Json.Str("+123456789012345678").as[Long].isLeft) && + assertTrue(Json.Str("Infinity").as[Long].isLeft) && + assertTrue(Json.Str("+Infinity").as[Long].isLeft) && + assertTrue(Json.Str("-Infinity").as[Long].isLeft) && + assertTrue(Json.Str("NaN").as[Long].isLeft) + }, + test("float") { + assert(Json.Num(Float.MinValue).as[Float])(isRight(equalTo(Float.MinValue))) && + assert(Json.Num(Float.MaxValue).as[Float])(isRight(equalTo(Float.MaxValue))) && + assert(Json.Str(Float.MinValue.toString).as[Float])(isRight(equalTo(Float.MinValue))) && + assert(Json.Str(Float.MaxValue.toString).as[Float])(isRight(equalTo(Float.MaxValue))) && + assert(Json.Str("Infinity").as[Float])(isRight(equalTo(Float.PositiveInfinity))) && + assert(Json.Str("+Infinity").as[Float])(isRight(equalTo(Float.PositiveInfinity))) && + assert(Json.Str("-Infinity").as[Float])(isRight(equalTo(Float.NegativeInfinity))) && + assertTrue(Json.Str("NaN").as[Float].isRight) && + assertTrue(Json.Str("\"-1.234567e9\"").as[Float].isLeft) && + assertTrue(Json.Str("\"1.234567e9\"").as[Float].isLeft) && + assertTrue(Json.Str("1.234567e9abc").as[Float].isLeft) && + assertTrue(Json.Str("+1.234567e9").as[Float].isLeft) + }, + test("double") { + assert(Json.Num(Double.MinValue).as[Double])(isRight(equalTo(Double.MinValue))) && + assert(Json.Num(Double.MaxValue).as[Double])(isRight(equalTo(Double.MaxValue))) && + assert(Json.Str(Double.MinValue.toString).as[Double])(isRight(equalTo(Double.MinValue))) && + assert(Json.Str(Double.MaxValue.toString).as[Double])(isRight(equalTo(Double.MaxValue))) && + assert(Json.Str("Infinity").as[Double])(isRight(equalTo(Double.PositiveInfinity))) && + assert(Json.Str("+Infinity").as[Double])(isRight(equalTo(Double.PositiveInfinity))) && + assert(Json.Str("-Infinity").as[Double])(isRight(equalTo(Double.NegativeInfinity))) && + assertTrue(Json.Str("NaN").as[Double].isRight) && + assertTrue(Json.Str("\"-1.23456789012345e9\"").as[Double].isLeft) && + assertTrue(Json.Str("\"1.23456789012345e9\"").as[Double].isLeft) && + assertTrue(Json.Str("1.23456789012345e9abc").as[Double].isLeft) && + assertTrue(Json.Str("+1.23456789012345e9").as[Double].isLeft) + }, + test("BigDecimal") { + assert(Json.Num(BigDecimal("-123.0e123")).as[BigDecimal])(isRight(equalTo(BigDecimal("-123.0e123")))) && + assert(Json.Num(BigDecimal("123.0e123")).as[BigDecimal])(isRight(equalTo(BigDecimal("123.0e123")))) && + assert(Json.Str("-123.0e123").as[BigDecimal])(isRight(equalTo(BigDecimal("-123.0e123")))) && + assert(Json.Str("123.0e123").as[BigDecimal])(isRight(equalTo(BigDecimal("123.0e123")))) && + assertTrue(Json.Str("123.0abc").as[BigDecimal].isLeft) && + assertTrue(Json.Str("Infinity").as[BigDecimal].isLeft) && + assertTrue(Json.Str("+Infinity").as[BigDecimal].isLeft) && + assertTrue(Json.Str("-Infinity").as[BigDecimal].isLeft) && + assertTrue(Json.Str("NaN").as[BigDecimal].isLeft) && + assert( + Json + .Str( + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" + ) + .as[BigDecimal] + )(isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)"))) && + assert(Json.Str("1.23456789012345678901e-2147483648").as[BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert(Json.Str("12345678901234567890.1e+2147483647").as[BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert(Json.Str("123456789012345678901e+2147483647").as[BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) + }, + test("java.math.BigDecimal") { + assert(Json.Num(BigDecimal("-123.0e123")).as[java.math.BigDecimal])( + isRight(equalTo(new java.math.BigDecimal("-123.0e123"))) + ) && + assert(Json.Num(BigDecimal("123.0e123")).as[java.math.BigDecimal])( + isRight(equalTo(new java.math.BigDecimal("123.0e123"))) + ) && + assert(Json.Str("-123.0e123").as[java.math.BigDecimal])( + isRight(equalTo(new java.math.BigDecimal("-123.0e123"))) + ) && + assert(Json.Str("123.0e123").as[java.math.BigDecimal])( + isRight(equalTo(new java.math.BigDecimal("123.0e123"))) + ) && + assertTrue(Json.Str("123.0abc").as[java.math.BigDecimal].isLeft) && + assertTrue(Json.Str("Infinity").as[java.math.BigDecimal].isLeft) && + assertTrue(Json.Str("+Infinity").as[java.math.BigDecimal].isLeft) && + assertTrue(Json.Str("-Infinity").as[java.math.BigDecimal].isLeft) && + assertTrue(Json.Str("NaN").as[java.math.BigDecimal].isLeft) && + assert( + Json + .Str( + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851" + ) + .as[java.math.BigDecimal] + )(isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)"))) && + assert(Json.Str("1.23456789012345678901e-2147483648").as[java.math.BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert(Json.Str("12345678901234567890.1e+2147483647").as[java.math.BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) && + assert(Json.Str("123456789012345678901e+2147483647").as[java.math.BigDecimal])( + isLeft(equalTo("(expected a BigDecimal with 256-bit mantissa)")) + ) + }, + test("BigInteger") { + assert(Json.Num(BigInt("170141183460469231731687303715884105728")).as[BigInteger])( + isRight(equalTo(new BigInteger("170141183460469231731687303715884105728"))) + ) && + assert(Json.Num(BigInt("-170141183460469231731687303715884105728")).as[BigInteger])( + isRight(equalTo(new BigInteger("-170141183460469231731687303715884105728"))) + ) && + assert(Json.Str("170141183460469231731687303715884105728").as[BigInteger])( + isRight(equalTo(new BigInteger("170141183460469231731687303715884105728"))) + ) && + assert(Json.Str("-170141183460469231731687303715884105728").as[BigInteger])( + isRight(equalTo(new BigInteger("-170141183460469231731687303715884105728"))) + ) && + assertTrue(Json.Str("123abc").as[BigInteger].isLeft) && + assertTrue(Json.Str("Infinity").as[BigInteger].isLeft) && + assertTrue(Json.Str("+Infinity").as[BigInteger].isLeft) && + assertTrue(Json.Str("-Infinity").as[BigInteger].isLeft) && + assertTrue(Json.Str("NaN").as[BigInteger].isLeft) && + assert( + Json + .Str( + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851316546851" + ) + .as[BigInteger] + )(isLeft(equalTo("(expected a 256-bit BigInteger)"))) && + assert( + Json + .Str("17014118346046923173168730371588410572848946516548466848651357486465481896465316846") + .as[BigInteger] + )(isLeft(equalTo("(expected a 256-bit BigInteger)"))) + }, + test("BigInt") { + assert(Json.Num(BigInt("170141183460469231731687303715884105728")).as[BigInt])( + isRight(equalTo(BigInt("170141183460469231731687303715884105728"))) + ) && + assert(Json.Num(BigInt("-170141183460469231731687303715884105728")).as[BigInt])( + isRight(equalTo(BigInt("-170141183460469231731687303715884105728"))) + ) && + assert(Json.Str("170141183460469231731687303715884105728").as[BigInt])( + isRight(equalTo(BigInt("170141183460469231731687303715884105728"))) + ) && + assert(Json.Str("-170141183460469231731687303715884105728").as[BigInt])( + isRight(equalTo(BigInt("-170141183460469231731687303715884105728"))) + ) && + assertTrue(Json.Str("123abc").as[BigInt].isLeft) && + assertTrue(Json.Str("Infinity").as[BigInt].isLeft) && + assertTrue(Json.Str("+Infinity").as[BigInt].isLeft) && + assertTrue(Json.Str("-Infinity").as[BigInt].isLeft) && + assertTrue(Json.Str("NaN").as[BigInt].isLeft) && + assert( + Json + .Str( + "170141183460469231731687303715884105728489465165484668486513574864654818964653168465316546851316546851" + ) + .as[BigInt] + )(isLeft(equalTo("(expected a 256-bit BigInt)"))) && + assert( + Json.Str("17014118346046923173168730371588410572848946516548466848651357486465481896465316846").as[BigInt] + )(isLeft(equalTo("(expected a 256-bit BigInt)"))) + }, test("eithers") { val bernies = List(Json.Obj("a" -> Json.Num(1)), Json.Obj("left" -> Json.Num(1)), Json.Obj("Left" -> Json.Num(1))) val trumps = List(Json.Obj("b" -> Json.Num(2)), Json.Obj("right" -> Json.Num(2)), Json.Obj("Right" -> Json.Num(2))) - - assert(bernies.map(_.as[Either[Int, Int]]))( - forall(isRight(isLeft(equalTo(1)))) - ) && assert(trumps.map(_.as[Either[Int, Int]]))( - forall(isRight(isRight(equalTo(2)))) - ) + assert(bernies.map(_.as[Either[Int, Int]]))(forall(isRight(isLeft(equalTo(1))))) && + assert(trumps.map(_.as[Either[Int, Int]]))(forall(isRight(isRight(equalTo(2))))) }, test("parameterless products") { import exampleproducts._ + assert(Json.Obj().as[Parameterless])(isRight(equalTo(Parameterless()))) && assert(Json.Null.as[Parameterless])(isRight(equalTo(Parameterless()))) && assert(Json.Obj("field" -> Json.Str("value")).as[Parameterless])(isRight(equalTo(Parameterless()))) From c1c2d7d5f240761d7994b79987f56a7b07f9ce1a Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 4 Mar 2025 14:16:41 +0100 Subject: [PATCH 201/311] More safe and efficient decoders for `java.time._` values (#1354) --- .../scala-2.x/zio/json/JsonFieldDecoder.scala | 2 +- .../scala-3/zio/json/JsonFieldDecoder.scala | 2 +- .../src/main/scala/zio/json/JsonDecoder.scala | 355 +- .../main/scala/zio/json/internal/lexer.scala | 45 +- .../scala/zio/json/javatime/parsers.scala | 1864 ++++------ .../src/test/scala/zio/json/DecoderSpec.scala | 64 +- .../test/scala/zio/json/JavaTimeSpec.scala | 3257 ++++------------- 7 files changed, 1956 insertions(+), 3633 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala index 32936ae45..5febd6f29 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/JsonFieldDecoder.scala @@ -69,7 +69,7 @@ object JsonFieldDecoder extends LowPriorityJsonFieldDecoder { def unsafeDecodeField(trace: List[JsonError], in: String): java.util.UUID = try UUIDParser.unsafeParse(in) catch { - case _: IllegalArgumentException => Lexer.error("expected UUID string", trace) + case _: IllegalArgumentException => Lexer.error("expected a UUID", trace) } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala index bacb87594..ac2d2e2dc 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonFieldDecoder.scala @@ -69,7 +69,7 @@ object JsonFieldDecoder extends LowPriorityJsonFieldDecoder { def unsafeDecodeField(trace: List[JsonError], in: String): java.util.UUID = try UUIDParser.unsafeParse(in) catch { - case _: IllegalArgumentException => Lexer.error("expected UUID string", trace) + case _: IllegalArgumentException => Lexer.error("expected a UUID", trace) } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 44c7b5c11..5c072aa2c 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -926,42 +926,301 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { import java.time._ - implicit val dayOfWeek: JsonDecoder[DayOfWeek] = javaTimeDecoder(s => DayOfWeek.valueOf(s.toUpperCase)) - implicit val duration: JsonDecoder[Duration] = javaTimeDecoder(parsers.unsafeParseDuration) - implicit val instant: JsonDecoder[Instant] = javaTimeDecoder(parsers.unsafeParseInstant) - implicit val localDate: JsonDecoder[LocalDate] = javaTimeDecoder(parsers.unsafeParseLocalDate) - implicit val localDateTime: JsonDecoder[LocalDateTime] = javaTimeDecoder(parsers.unsafeParseLocalDateTime) - implicit val localTime: JsonDecoder[LocalTime] = javaTimeDecoder(parsers.unsafeParseLocalTime) - implicit val month: JsonDecoder[Month] = javaTimeDecoder(s => Month.valueOf(s.toUpperCase)) - implicit val monthDay: JsonDecoder[MonthDay] = javaTimeDecoder(parsers.unsafeParseMonthDay) - implicit val offsetDateTime: JsonDecoder[OffsetDateTime] = javaTimeDecoder(parsers.unsafeParseOffsetDateTime) - implicit val offsetTime: JsonDecoder[OffsetTime] = javaTimeDecoder(parsers.unsafeParseOffsetTime) - implicit val period: JsonDecoder[Period] = javaTimeDecoder(parsers.unsafeParsePeriod) - implicit val year: JsonDecoder[Year] = javaTimeDecoder(parsers.unsafeParseYear) - implicit val yearMonth: JsonDecoder[YearMonth] = javaTimeDecoder(parsers.unsafeParseYearMonth) - implicit val zonedDateTime: JsonDecoder[ZonedDateTime] = javaTimeDecoder(parsers.unsafeParseZonedDateTime) - implicit val zoneId: JsonDecoder[ZoneId] = javaTimeDecoder(parsers.unsafeParseZoneId) - implicit val zoneOffset: JsonDecoder[ZoneOffset] = javaTimeDecoder(parsers.unsafeParseZoneOffset) - - private[this] def javaTimeDecoder[A](f: String => A): JsonDecoder[A] = new JsonDecoder[A] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = - parseJavaTime(trace, Lexer.string(trace, in).toString) - - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + implicit val dayOfWeek: JsonDecoder[DayOfWeek] = new JsonDecoder[DayOfWeek] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): DayOfWeek = Lexer.dayOfWeek(trace, in) + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): DayOfWeek = { json match { - case s: Json.Str => parseJavaTime(trace, s.value) - case _ => Lexer.error("expected string", trace) + case s: Json.Str => + try return DayOfWeek.valueOf(s.value.toUpperCase) + catch { + case _: IllegalArgumentException => + } + case _ => + } + Lexer.error("expected a DayOfWeek", trace) + } + } + implicit val duration: JsonDecoder[Duration] = new JsonDecoder[Duration] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Duration = + try parsers.unsafeParseDuration(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a Duration", trace) } - // Commonized handling for decoding from string to java.time Class - @inline private[this] def parseJavaTime(trace: List[JsonError], s: String): A = - try f(s) + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Duration = { + json match { + case s: Json.Str => + try return parsers.unsafeParseDuration(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a Duration", trace) + } + } + implicit val instant: JsonDecoder[Instant] = new JsonDecoder[Instant] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Instant = + try parsers.unsafeParseInstant(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected an Instant", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Instant = { + json match { + case s: Json.Str => + try return parsers.unsafeParseInstant(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected an Instant", trace) + } + } + implicit val localDate: JsonDecoder[LocalDate] = new JsonDecoder[LocalDate] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): LocalDate = + try parsers.unsafeParseLocalDate(Lexer.string(trace, in).toString) catch { - case ex: DateTimeException => - Lexer.error(s"${strip(s)} is not a valid ISO-8601 format, ${ex.getMessage}", trace) - case _: IllegalArgumentException => - Lexer.error(s"${strip(s)} is not a valid ISO-8601 format", trace) + case _: DateTimeException => Lexer.error("expected a LocalDate", trace) } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): LocalDate = { + json match { + case s: Json.Str => + try return parsers.unsafeParseLocalDate(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a LocalDate", trace) + } + } + implicit val localDateTime: JsonDecoder[LocalDateTime] = new JsonDecoder[LocalDateTime] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): LocalDateTime = + try parsers.unsafeParseLocalDateTime(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a LocalDateTime", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): LocalDateTime = { + json match { + case s: Json.Str => + try return parsers.unsafeParseLocalDateTime(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a LocalDateTime", trace) + } + } + implicit val localTime: JsonDecoder[LocalTime] = new JsonDecoder[LocalTime] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): LocalTime = + try parsers.unsafeParseLocalTime(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a LocalTime", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): LocalTime = { + json match { + case s: Json.Str => + try return parsers.unsafeParseLocalTime(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a LocalTime", trace) + } + } + implicit val month: JsonDecoder[Month] = new JsonDecoder[Month] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Month = Lexer.month(trace, in) + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Month = { + json match { + case s: Json.Str => + try return Month.valueOf(s.value.toUpperCase) + catch { + case _: IllegalArgumentException => + } + case _ => + } + Lexer.error("expected a Month", trace) + } + } + implicit val monthDay: JsonDecoder[MonthDay] = new JsonDecoder[MonthDay] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): MonthDay = + try parsers.unsafeParseMonthDay(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a MonthDay", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): MonthDay = { + json match { + case s: Json.Str => + try return parsers.unsafeParseMonthDay(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a MonthDay", trace) + } + } + implicit val offsetDateTime: JsonDecoder[OffsetDateTime] = new JsonDecoder[OffsetDateTime] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): OffsetDateTime = + try parsers.unsafeParseOffsetDateTime(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected an OffsetDateTime", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): OffsetDateTime = { + json match { + case s: Json.Str => + try return parsers.unsafeParseOffsetDateTime(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected an OffsetDateTime", trace) + } + } + implicit val offsetTime: JsonDecoder[OffsetTime] = new JsonDecoder[OffsetTime] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): OffsetTime = + try parsers.unsafeParseOffsetTime(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected an OffsetTime", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): OffsetTime = { + json match { + case s: Json.Str => + try return parsers.unsafeParseOffsetTime(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected an OffsetTime", trace) + } + } + implicit val period: JsonDecoder[Period] = new JsonDecoder[Period] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Period = + try parsers.unsafeParsePeriod(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a Period", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Period = { + json match { + case s: Json.Str => + try return parsers.unsafeParsePeriod(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a Period", trace) + } + } + implicit val year: JsonDecoder[Year] = new JsonDecoder[Year] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): Year = + try parsers.unsafeParseYear(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a Year", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Year = { + json match { + case s: Json.Str => + try return parsers.unsafeParseYear(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a Year", trace) + } + } + implicit val yearMonth: JsonDecoder[YearMonth] = new JsonDecoder[YearMonth] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): YearMonth = + try parsers.unsafeParseYearMonth(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a YearMonth", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): YearMonth = { + json match { + case s: Json.Str => + try return parsers.unsafeParseYearMonth(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a YearMonth", trace) + } + } + implicit val zonedDateTime: JsonDecoder[ZonedDateTime] = new JsonDecoder[ZonedDateTime] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): ZonedDateTime = + try parsers.unsafeParseZonedDateTime(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a ZonedDateTime", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): ZonedDateTime = { + json match { + case s: Json.Str => + try return parsers.unsafeParseZonedDateTime(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a ZonedDateTime", trace) + } + } + implicit val zoneId: JsonDecoder[ZoneId] = new JsonDecoder[ZoneId] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): ZoneId = + try parsers.unsafeParseZoneId(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a ZoneId", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): ZoneId = { + json match { + case s: Json.Str => + try return parsers.unsafeParseZoneId(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a ZoneId", trace) + } + } + implicit val zoneOffset: JsonDecoder[ZoneOffset] = new JsonDecoder[ZoneOffset] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): ZoneOffset = + try parsers.unsafeParseZoneOffset(Lexer.string(trace, in).toString) + catch { + case _: DateTimeException => Lexer.error("expected a ZoneOffset", trace) + } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): ZoneOffset = { + json match { + case s: Json.Str => + try return parsers.unsafeParseZoneOffset(s.value) + catch { + case _: DateTimeException => + } + case _ => + } + Lexer.error("expected a ZoneOffset", trace) + } } // FIXME: remove in the next major version @@ -977,34 +1236,40 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { implicit val uuid: JsonDecoder[UUID] = new JsonDecoder[UUID] { def unsafeDecode(trace: List[JsonError], in: RetractReader): UUID = Lexer.uuid(trace, in) - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID = + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): UUID = { json match { case s: Json.Str => - try UUIDParser.unsafeParse(s.value) + try return UUIDParser.unsafeParse(s.value) catch { - case _: IllegalArgumentException => Lexer.error("expected UUID string", trace) + case _: IllegalArgumentException => } - case _ => Lexer.error("expected UUID string", trace) + case _ => } + Lexer.error("expected a UUID", trace) + } } implicit val currency: JsonDecoder[java.util.Currency] = new JsonDecoder[java.util.Currency] { def unsafeDecode(trace: List[JsonError], in: RetractReader): java.util.Currency = - parseCurrency(trace, Lexer.string(trace, in).toString) - - override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.util.Currency = - json match { - case s: Json.Str => parseCurrency(trace, s.value) - case _ => Lexer.error("expected string", trace) + try java.util.Currency.getInstance(Lexer.string(trace, in).toString) + catch { + case _: IllegalArgumentException => Lexer.error("expected a Currency", trace) } - @inline private[this] def parseCurrency(trace: List[JsonError], s: String): java.util.Currency = - try java.util.Currency.getInstance(s) - catch { - case _: IllegalArgumentException => Lexer.error(s"Invalid Currency: ${strip(s)}", trace) + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): java.util.Currency = { + json match { + case s: Json.Str => + try return java.util.Currency.getInstance(s.value) + catch { + case _: IllegalArgumentException => + } + case _ => } + Lexer.error("expected a Currency", trace) + } } + // FIXME: remove in the next major version @noinline private[json] def strip(s: String, len: Int = 50): String = if (s.length <= len) s else s.substring(0, len) + "..." diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index e242c4320..15e757e3b 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -17,6 +17,7 @@ package zio.json.internal import zio.json.JsonDecoder.{ JsonError, UnsafeJson } +import java.time.{ DayOfWeek, Month } import java.util.UUID import scala.annotation._ @@ -323,7 +324,7 @@ object Lexer { uuidError(trace) } - @noinline private[this] def uuidError(trace: List[JsonError]): Nothing = error("expected UUID string", trace) + @noinline private[this] def uuidError(trace: List[JsonError]): Nothing = error("expected a UUID", trace) private[this] val charArrays = new ThreadLocal[Array[Char]] { override def initialValue(): Array[Char] = new Array[Char](1024) // should be longer than 256 @@ -487,6 +488,48 @@ object Lexer { case UnsafeNumbers.UnsafeNumber => error(s"expected a BigDecimal with $NumberMaxBits-bit mantissa", trace) } + def dayOfWeek(trace: List[JsonError], in: OneCharReader): DayOfWeek = { + var c = in.nextNonWhitespace() + if (c == '"') { + var bs = dayOfWeekMatrix.initial + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + bs = dayOfWeekMatrix.update(bs, i, (c & 0xffdf).toChar) + i += 1 + } + val dayOfWeek = dayOfWeekMatrix.first(dayOfWeekMatrix.exact(bs, i)) + 1 + if (dayOfWeek > 0) return DayOfWeek.of(dayOfWeek) + } + error("expected a DayOfWeek", trace) + } + + private[this] val dayOfWeekMatrix = new StringMatrix(DayOfWeek.values.map(_.toString)) + + def month(trace: List[JsonError], in: OneCharReader): Month = { + var c = in.nextNonWhitespace() + if (c == '"') { + var bs = monthMatrix.initial + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + bs = monthMatrix.update(bs, i, (c & 0xffdf).toChar) + i += 1 + } + val month = monthMatrix.first(monthMatrix.exact(bs, i)) + 1 + if (month > 0) return Month.of(month) + } + error("expected a Month", trace) + } + + private[this] val monthMatrix = new StringMatrix(Month.values.map(_.toString)) + @inline def char(trace: List[JsonError], in: OneCharReader, c: Char): Unit = { val got = in.nextNonWhitespace() if (got != c) error(s"'$c'", got, trace) diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala index 0325448e9..6e63c1171 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala @@ -15,25 +15,8 @@ */ package zio.json.javatime -import java.time.{ - DateTimeException, - Duration, - Instant, - LocalDate, - LocalDateTime, - LocalTime, - MonthDay, - OffsetDateTime, - OffsetTime, - Period, - Year, - YearMonth, - ZoneId, - ZoneOffset, - ZonedDateTime -} +import java.time._ import java.util.concurrent.ConcurrentHashMap -import scala.annotation.switch import scala.util.control.NoStackTrace private[json] object parsers { @@ -45,40 +28,38 @@ private[json] object parsers { var pos = 0 var seconds = 0L var nanos, state = 0 - if (pos >= len) durationError(pos) + if (pos >= len) durationError() var ch = input.charAt(pos) pos += 1 val isNeg = ch == '-' if (isNeg) { - if (pos >= len) durationError(pos) + if (pos >= len) durationError() ch = input.charAt(pos) pos += 1 } - if (ch != 'P') durationOrPeriodStartError(isNeg, pos - 1) - if (pos >= len) durationError(pos) + if (ch != 'P' || pos >= len) durationError() ch = input.charAt(pos) pos += 1 while ({ if (state == 0) { if (ch == 'T') { - if (pos >= len) durationError(pos) + if (pos >= len) durationError() ch = input.charAt(pos) pos += 1 state = 1 } } else if (state == 1) { - if (ch != 'T') charsError('T', '"', pos - 1) - if (pos >= len) durationError(pos) + if (ch != 'T' || pos >= len) durationError() ch = input.charAt(pos) pos += 1 - } else if (state == 4 && pos >= len) durationError(pos - 1) + } else if (state == 4 && pos >= len) durationError() val isNegX = ch == '-' if (isNegX) { - if (pos >= len) durationError(pos) + if (pos >= len) durationError() ch = input.charAt(pos) pos += 1 } - if (ch < '0' || ch > '9') durationOrPeriodDigitError(isNegX, state <= 1, pos - 1) + if (ch < '0' || ch > '9') durationError() var x: Long = ('0' - ch).toLong while ( (pos < len) && { @@ -91,31 +72,28 @@ private[json] object parsers { x = x * 10 + ('0' - ch) x > 0 } - ) durationError(pos) + ) durationError() pos += 1 } if (!(isNeg ^ isNegX)) { - if (x == -9223372036854775808L) durationError(pos) + if (x == -9223372036854775808L) durationError() x = -x } if (ch == 'D' && state <= 0) { - if (x < -106751991167300L || x > 106751991167300L) - durationError(pos) // -106751991167300L == Long.MinValue / 86400 + if (x < -106751991167300L || x > 106751991167300L) durationError() seconds = x * 86400 state = 1 } else if (ch == 'H' && state <= 1) { - if (x < -2562047788015215L || x > 2562047788015215L) - durationError(pos) // -2562047788015215L == Long.MinValue / 3600 - seconds = sumSeconds(x * 3600, seconds, pos) + if (x < -2562047788015215L || x > 2562047788015215L) durationError() + seconds = sumSeconds(x * 3600, seconds) state = 2 } else if (ch == 'M' && state <= 2) { - if (x < -153722867280912930L || x > 153722867280912930L) - durationError(pos) // -153722867280912930L == Long.MinValue / 60 - seconds = sumSeconds(x * 60, seconds, pos) + if (x < -153722867280912930L || x > 153722867280912930L) durationError() + seconds = sumSeconds(x * 60, seconds) state = 3 } else if (ch == '.') { pos += 1 - seconds = sumSeconds(x, seconds, pos) + seconds = sumSeconds(x, seconds) var nanoDigitWeight = 100000000 while ( (pos < len) && { @@ -127,13 +105,13 @@ private[json] object parsers { nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 pos += 1 } - if (ch != 'S') nanoError(nanoDigitWeight, 'S', pos) + if (ch != 'S') durationError() if (isNeg ^ isNegX) nanos = -nanos state = 4 } else if (ch == 'S') { - seconds = sumSeconds(x, seconds, pos) + seconds = sumSeconds(x, seconds) state = 4 - } else durationError(state, pos) + } else durationError() pos += 1 (pos < len) && { ch = input.charAt(pos) @@ -145,126 +123,97 @@ private[json] object parsers { } def unsafeParseInstant(input: String): Instant = { - val len = input.length - var pos = 0 - val year = { - if (pos + 4 >= len) instantError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val ch3 = input.charAt(pos + 3) - val ch4 = input.charAt(pos + 4) - if (ch0 >= '0' && ch0 <= '9') { - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 != '-') charError('-', pos + 4) - pos += 5 - ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 - } else { - val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + val len = input.length + var pos, year, month, day = 0 + if ( + pos + 4 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) pos += 5 - var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 - var yearDigits = 4 - var ch: Char = '0' - while ({ - if (pos >= len) instantError(pos) - ch = input.charAt(pos) - pos += 1 - ch >= '0' && ch <= '9' && yearDigits < 10 - }) { - year = - if (year > 100000000) 2147483647 - else year * 10 + (ch - '0') - yearDigits += 1 - } - if (yearDigits == 10 && year > 1000000000) yearError(pos - 2) - if (yearNeg) { - if (year == 0) yearError(pos - 2) - year = -year + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + val yearNeg = ch0 == '-' || (ch0 != '+' && instantError()) + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 10 + } + ) { + year = + if (year > 100000000) 2147483647 + else year * 10 + (ch - '0') + yearDigits += 1 + } + yearDigits == 10 && year > 1000000000 || yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } } - if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) - year + } || pos + 5 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + val ch5 = input.charAt(pos + 5) + pos += 6 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || ch5 != 'T' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForYearMonth(year, month)) } - } - val month = { - if (pos + 2 >= len) instantError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (month < 1 || month > 12) monthError(pos + 1) - if (ch2 != '-') charError('-', pos + 2) - pos += 3 - month - } - val day = { - if (pos + 2 >= len) instantError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) - if (ch2 != 'T') charError('T', pos + 2) - pos += 3 - day - } + ) instantError() val epochDay = epochDayForYear(year) + (dayOfYearForYearMonth(year, month) + day - 719529) // 719528 == days 0000 to 1970 - var epochSecond = { - if (pos + 2 >= len) instantError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (hour > 23) hourError(pos + 1) - if (ch2 != ':') charError(':', pos + 2) - pos += 3 - hour * 3600 - } - epochSecond += { - if (pos + 1 >= len) instantError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') minuteError(pos + 1) - pos += 2 - (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 - } - var nanoDigitWeight = -1 - var nano = 0 - var ch = (0: Char) + var epochSecond = 0 + if ( + pos + 4 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + pos += 5 + val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + epochSecond = hour * 3600 + (ch3 * 10 + ch4 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } + ) instantError() + var nano = 0 + var ch = '0' if (pos < len) { ch = input.charAt(pos) pos += 1 if (ch == ':') { - nanoDigitWeight = -2 - epochSecond += { - if (pos + 1 >= len) instantError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') secondError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + epochSecond += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) instantError() if (pos < len) { ch = input.charAt(pos) pos += 1 if (ch == '.') { - nanoDigitWeight = 100000000 + var nanoDigitWeight = 100000000 while ( pos < len && { ch = input.charAt(pos) @@ -281,18 +230,16 @@ private[json] object parsers { } var offsetTotal = 0 if (ch != 'Z') { - val offsetNeg = ch == '-' || (ch != '+' && timezoneSignError(nanoDigitWeight, pos - 1)) - offsetTotal = { - if (pos + 1 >= len) instantError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val offsetHour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (offsetHour > 18) timezoneOffsetHourError(pos + 1) - pos += 2 - offsetHour * 3600 - } + val offsetNeg = ch == '-' || (ch != '+' && instantError()) + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' + } + ) instantError() if ( pos < len && { ch = input.charAt(pos) @@ -300,16 +247,15 @@ private[json] object parsers { ch == ':' } ) { - offsetTotal += { - if (pos + 1 >= len) instantError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetMinuteError(pos + 1) - pos += 2 - (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 - } + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) instantError() if ( pos < len && { ch = input.charAt(pos) @@ -317,429 +263,348 @@ private[json] object parsers { ch == ':' } ) { - offsetTotal += { - if (pos + 1 >= len) instantError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetSecondError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) instantError() } } - if (offsetTotal > 64800) zoneOffsetError(pos) // 64800 == 18 * 60 * 60 + if (offsetTotal > 64800) instantError() // 64800 == 18 * 60 * 60 if (offsetNeg) offsetTotal = -offsetTotal } - if (pos != len) instantError(pos) - Instant.ofEpochSecond( - epochDay * 86400 + (epochSecond - offsetTotal), - nano.toLong - ) // 86400 == seconds per day + if (pos != len) instantError() + Instant.ofEpochSecond(epochDay * 86400 + (epochSecond - offsetTotal), nano.toLong) } def unsafeParseLocalDate(input: String): LocalDate = { - val len = input.length - var pos = 0 - val year = { - if (pos + 4 >= len) localDateError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val ch3 = input.charAt(pos + 3) - val ch4 = input.charAt(pos + 4) - if (ch0 >= '0' && ch0 <= '9') { - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 != '-') charError('-', pos + 4) - pos += 5 - ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 - } else { - val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + val len = input.length + var pos, year, month, day = 0 + if ( + pos + 4 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) pos += 5 - var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 - var yearDigits = 4 - var ch: Char = '0' - while ({ - if (pos >= len) localDateError(pos) - ch = input.charAt(pos) - pos += 1 - ch >= '0' && ch <= '9' && yearDigits < 9 - }) { - year = year * 10 + (ch - '0') - yearDigits += 1 - } - if (yearNeg) { - if (year == 0) yearError(pos - 2) - year = -year + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + val yearNeg = ch0 == '-' || (ch0 != '+' && localDateError()) + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + } + ) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } } - if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) - year + } || pos + 5 != len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + pos += 5 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForYearMonth(year, month)) } - } - val month = { - if (pos + 2 >= len) localDateError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (month < 1 || month > 12) monthError(pos + 1) - if (ch2 != '-') charError('-', pos + 2) - pos += 3 - month - } - val day = { - if (pos + 1 >= len) localDateError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) - pos += 2 - day - } - if (pos != len) localDateError(pos) + ) localDateError() LocalDate.of(year, month, day) } def unsafeParseLocalDateTime(input: String): LocalDateTime = { - val len = input.length - var pos = 0 - val year = { - if (pos + 4 >= len) localDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val ch3 = input.charAt(pos + 3) - val ch4 = input.charAt(pos + 4) - if (ch0 >= '0' && ch0 <= '9') { - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 != '-') charError('-', pos + 4) - pos += 5 - ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 - } else { - val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + val len = input.length + var pos, year, month, day = 0 + if ( + pos + 4 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) pos += 5 - var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 - var yearDigits = 4 - var ch: Char = '0' - while ({ - if (pos >= len) localDateTimeError(pos) - ch = input.charAt(pos) - pos += 1 - ch >= '0' && ch <= '9' && yearDigits < 9 - }) { - year = year * 10 + (ch - '0') - yearDigits += 1 - } - if (yearNeg) { - if (year == 0) yearError(pos - 2) - year = -year + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + val yearNeg = ch0 == '-' || (ch0 != '+' && localDateTimeError()) + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + } + ) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } } - if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) - year + } || pos + 5 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + val ch5 = input.charAt(pos + 5) + pos += 6 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || ch5 != 'T' || day == 0 || month < 1 || month > 12 || + (day > 28 && day > maxDayForYearMonth(year, month)) } - } - val month = { - if (pos + 2 >= len) localDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (month < 1 || month > 12) monthError(pos + 1) - if (ch2 != '-') charError('-', pos + 2) - pos += 3 - month - } - val day = { - if (pos + 2 >= len) localDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) - if (ch2 != 'T') charError('T', pos + 2) - pos += 3 - day - } - val hour = { - if (pos + 2 >= len) localDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (hour > 23) hourError(pos + 1) - if (ch2 != ':') charError(':', pos + 2) - pos += 3 - hour - } - val minute = { - if (pos + 1 >= len) localDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') minuteError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } - var second, nano = 0 - if (pos < len) { - if (input.charAt(pos) != ':') charError(':', pos) - pos += 1 - second = { - if (pos + 1 >= len) localDateTimeError(pos) + ) localDateTimeError() + var hour, minute = 0 + if ( + pos + 4 >= len || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') secondError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 } + ) localDateTimeError() + var second, nano = 0 + if (pos < len) { + if ( + input.charAt(pos) != ':' || { + pos += 1 + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + } + ) localTimeError() if (pos < len) { - if (input.charAt(pos) != '.') charError('.', pos) - pos += 1 - var nanoDigitWeight = 100000000 - var ch = '0' - while ( - pos < len && { - ch = input.charAt(pos) + if ( + input.charAt(pos) != '.' || { pos += 1 - ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + var nanoDigitWeight = 100000000 + var ch = '0' + while ( + pos < len && { + ch = input.charAt(pos) + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + pos += 1 + } + pos != len } - ) { - nano += (ch - '0') * nanoDigitWeight - nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 - } - if (pos != len || ch < '0' || ch > '9') localDateTimeError(pos - 1) + ) localDateTimeError() } } LocalDateTime.of(year, month, day, hour, minute, second, nano) } def unsafeParseLocalTime(input: String): LocalTime = { - val len = input.length - var pos = 0 - val hour = { - if (pos + 2 >= len) localTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (hour > 23) hourError(pos + 1) - if (ch2 != ':') charError(':', pos + 2) - pos += 3 - hour - } - val minute = { - if (pos + 1 >= len) localTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') minuteError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } - var second, nano = 0 - if (pos < len) { - if (input.charAt(pos) != ':') charError(':', pos) - pos += 1 - second = { - if (pos + 1 >= len) localTimeError(pos) + val len = input.length + var pos, hour, minute = 0 + if ( + pos + 4 >= len || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') secondError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 } + ) localTimeError() + var second, nano = 0 + if (pos < len) { + if ( + input.charAt(pos) != ':' || { + pos += 1 + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + } + ) localTimeError() if (pos < len) { - if (input.charAt(pos) != '.') charError('.', pos) - pos += 1 - var nanoDigitWeight = 100000000 - var ch = '0' - while ( - pos < len && { - ch = input.charAt(pos) + if ( + input.charAt(pos) != '.' || { pos += 1 - ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + var nanoDigitWeight = 100000000 + var ch = '0' + while ( + pos < len && { + ch = input.charAt(pos) + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + pos += 1 + } + pos != len } - ) { - nano += (ch - '0') * nanoDigitWeight - nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 - } - if (pos != len || ch < '0' || ch > '9') localTimeError(pos - 1) + ) localTimeError() } } LocalTime.of(hour, minute, second, nano) } def unsafeParseMonthDay(input: String): MonthDay = { - if (input.length != 7) error("illegal month day", 0) - val ch0 = input.charAt(0) - val ch1 = input.charAt(1) - val ch2 = input.charAt(2) - val ch3 = input.charAt(3) - val ch4 = input.charAt(4) - val ch5 = input.charAt(5) - val ch6 = input.charAt(6) - val month = ch2 * 10 + ch3 - 528 // 528 == '0' * 11 - val day = ch5 * 10 + ch6 - 528 // 528 == '0' * 11 - if (ch0 != '-') charError('-', 0) - if (ch1 != '-') charError('-', 1) - if (ch2 < '0' || ch2 > '9') digitError(2) - if (ch3 < '0' || ch3 > '9') digitError(3) - if (month < 1 || month > 12) monthError(3) - if (ch4 != '-') charError('-', 4) - if (ch5 < '0' || ch5 > '9') digitError(5) - if (ch6 < '0' || ch6 > '9') digitError(6) - if (day == 0 || (day > 28 && day > maxDayForMonth(month))) dayError(6) + var month, day = 0 + if ( + input.length != 7 || { + val ch0 = input.charAt(0) + val ch1 = input.charAt(1) + val ch2 = input.charAt(2) + val ch3 = input.charAt(3) + val ch4 = input.charAt(4) + val ch5 = input.charAt(5) + val ch6 = input.charAt(6) + month = ch2 * 10 + ch3 - 528 // 528 == '0' * 11 + day = ch5 * 10 + ch6 - 528 // 528 == '0' * 11 + ch0 != '-' || ch1 != '-' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || ch4 != '-' || + ch5 < '0' || ch5 > '9' || ch6 < '0' || ch6 > '9' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForMonth(month)) + } + ) monthDayError() MonthDay.of(month, day) } def unsafeParseOffsetDateTime(input: String): OffsetDateTime = { - val len = input.length - var pos = 0 - val year = { - if (pos + 4 >= len) offsetDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val ch3 = input.charAt(pos + 3) - val ch4 = input.charAt(pos + 4) - if (ch0 >= '0' && ch0 <= '9') { - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 != '-') charError('-', pos + 4) - pos += 5 - ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 - } else { - val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + val len = input.length + var pos, year, month, day = 0 + if ( + pos + 4 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) pos += 5 - var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 - var yearDigits = 4 - var ch: Char = '0' - while ({ - if (pos >= len) offsetDateTimeError(pos) - ch = input.charAt(pos) - pos += 1 - ch >= '0' && ch <= '9' && yearDigits < 9 - }) { - year = year * 10 + (ch - '0') - yearDigits += 1 - } - if (yearNeg) { - if (year == 0) yearError(pos - 2) - year = -year + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + val yearNeg = ch0 == '-' || (ch0 != '+' && offsetDateTimeError()) + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ( + pos < len && { + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + } + ) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } } - if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) - year - } - } - val month = { - if (pos + 2 >= len) offsetDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (month < 1 || month > 12) monthError(pos + 1) - if (ch2 != '-') charError('-', pos + 2) - pos += 3 - month - } - val day = { - if (pos + 2 >= len) offsetDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) - if (ch2 != 'T') charError('T', pos + 2) - pos += 3 - day - } - val hour = { - if (pos + 2 >= len) offsetDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (hour > 23) hourError(pos + 1) - if (ch2 != ':') charError(':', pos + 2) - pos += 3 - hour - } - val minute = { - if (pos + 1 >= len) offsetDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') minuteError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } - var second, nano = 0 - var nanoDigitWeight = -1 - if (pos >= len) timezoneSignError(nanoDigitWeight, pos) - var ch = input.charAt(pos) - pos += 1 - if (ch == ':') { - nanoDigitWeight = -2 - second = { - if (pos + 1 >= len) offsetDateTimeError(pos) + } || pos + 5 >= len || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') secondError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + val ch5 = input.charAt(pos + 5) + pos += 6 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || ch5 != 'T' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForYearMonth(year, month)) } - if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + ) offsetDateTimeError() + var hour, minute = 0 + if ( + pos + 4 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } || pos >= len + ) offsetDateTimeError() + var second, nano = 0 + var ch = input.charAt(pos) + pos += 1 + if (ch == ':') { + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } || pos >= len + ) offsetDateTimeError() ch = input.charAt(pos) pos += 1 if (ch == '.') { - nanoDigitWeight = 100000000 + var nanoDigitWeight = 100000000 while ({ - if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + if (pos >= len) offsetDateTimeError() ch = input.charAt(pos) pos += 1 ch >= '0' && ch <= '9' && nanoDigitWeight != 0 @@ -752,111 +617,89 @@ private[json] object parsers { val zoneOffset = if (ch == 'Z') ZoneOffset.UTC else { - val offsetNeg = ch == '-' || (ch != '+' && timezoneSignError(nanoDigitWeight, pos - 1)) - val offsetHour = { - if (pos + 1 >= len) offsetDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val offsetHour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (offsetHour > 18) timezoneOffsetHourError(pos + 1) - pos += 2 - offsetHour - } - var offsetMinute, offsetSecond = 0 + val offsetNeg = ch == '-' || (ch != '+' && offsetDateTimeError()) + var offsetTotal = 0 if ( - pos < len && { - ch = input.charAt(pos) - pos += 1 - ch == ':' - } - ) { - offsetMinute = { - if (pos + 1 >= len) offsetDateTimeError(pos) + pos + 1 >= len || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetMinuteError(pos + 1) pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' } + ) offsetDateTimeError() + if (pos < len) { if ( - pos < len && { - ch = input.charAt(pos) + input.charAt(pos) != ':' || { pos += 1 - ch == ':' - } - ) { - offsetSecond = { - if (pos + 1 >= len) offsetDateTimeError(pos) + pos + 1 >= len + } || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetSecondError(pos + 1) pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' } + ) offsetDateTimeError() + if (pos < len) { + if ( + input.charAt(pos) != ':' || { + pos += 1 + pos + 1 >= len + } || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) offsetDateTimeError() } } - toZoneOffset(offsetNeg, offsetHour, offsetMinute, offsetSecond, pos) + if (offsetTotal > 64800) offsetDateTimeError() + toZoneOffset(offsetNeg, offsetTotal) } - if (pos != len) offsetDateTimeError(pos) + if (pos != len) offsetDateTimeError() OffsetDateTime.of(year, month, day, hour, minute, second, nano, zoneOffset) } def unsafeParseOffsetTime(input: String): OffsetTime = { - val len = input.length - var pos = 0 - val hour = { - if (pos + 2 >= len) offsetTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (hour > 23) hourError(pos + 1) - if (ch2 != ':') charError(':', pos + 2) - pos += 3 - hour - } - val minute = { - if (pos + 1 >= len) offsetTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') minuteError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } - var second, nano = 0 - var nanoDigitWeight = -1 - if (pos >= len) timezoneSignError(nanoDigitWeight, pos) - var ch = input.charAt(pos) - pos += 1 - if (ch == ':') { - nanoDigitWeight = -2 - second = { - if (pos + 1 >= len) offsetTimeError(pos) + val len = input.length + var pos = 0 + var hour, minute = 0 + if ( + pos + 4 >= len || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') secondError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } - if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } || pos >= len + ) offsetTimeError() + var second, nano = 0 + var ch = input.charAt(pos) + pos += 1 + if (ch == ':') { + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } || pos >= len + ) offsetTimeError() ch = input.charAt(pos) pos += 1 if (ch == '.') { - nanoDigitWeight = 100000000 + var nanoDigitWeight = 100000000 while ({ - if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + if (pos >= len) offsetTimeError() ch = input.charAt(pos) pos += 1 ch >= '0' && ch <= '9' && nanoDigitWeight != 0 @@ -869,88 +712,76 @@ private[json] object parsers { val zoneOffset = if (ch == 'Z') ZoneOffset.UTC else { - val offsetNeg = ch == '-' || (ch != '+' && timezoneSignError(nanoDigitWeight, pos - 1)) - nanoDigitWeight = -3 - val offsetHour = { - if (pos + 1 >= len) offsetTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val offsetHour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (offsetHour > 18) timezoneOffsetHourError(pos + 1) - pos += 2 - offsetHour - } - var offsetMinute, offsetSecond = 0 + val offsetNeg = ch == '-' || (ch != '+' && offsetTimeError()) + var offsetTotal = 0 if ( - pos < len && { - ch = input.charAt(pos) - pos += 1 - ch == ':' - } - ) { - offsetMinute = { - if (pos + 1 >= len) offsetTimeError(pos) + pos + 1 >= len || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetMinuteError(pos + 1) pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' } + ) offsetTimeError() + if (pos < len) { if ( - pos < len && { - ch = input.charAt(pos) + input.charAt(pos) != ':' || { pos += 1 - ch == ':' - } - ) { - nanoDigitWeight = -4 - offsetSecond = { - if (pos + 1 >= len) offsetTimeError(pos) + pos + 1 >= len + } || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetSecondError(pos + 1) pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' } + ) offsetTimeError() + if (pos < len) { + if ( + input.charAt(pos) != ':' || { + pos += 1 + pos + 1 >= len + } || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) offsetTimeError() } } - toZoneOffset(offsetNeg, offsetHour, offsetMinute, offsetSecond, pos) + if (offsetTotal > 64800) offsetTimeError() + toZoneOffset(offsetNeg, offsetTotal) } - if (pos != len) offsetTimeError(pos) + if (pos != len) offsetTimeError() OffsetTime.of(hour, minute, second, nano, zoneOffset) } def unsafeParsePeriod(input: String): Period = { val len = input.length var pos, state, years, months, days = 0 - if (pos >= len) periodError(pos) + if (pos >= len) periodError() var ch = input.charAt(pos) pos += 1 val isNeg = ch == '-' if (isNeg) { - if (pos >= len) periodError(pos) + if (pos >= len) periodError() ch = input.charAt(pos) pos += 1 } - if (ch != 'P') durationOrPeriodStartError(isNeg, pos - 1) - if (pos >= len) periodError(pos) + if (ch != 'P' || pos >= len) periodError() ch = input.charAt(pos) pos += 1 while ({ - if (state == 4 && pos >= len) periodError(pos - 1) + if (state == 4 && pos >= len) periodError() val isNegX = ch == '-' if (isNegX) { - if (pos >= len) periodError(pos) + if (pos >= len) periodError() ch = input.charAt(pos) pos += 1 } - if (ch < '0' || ch > '9') durationOrPeriodDigitError(isNegX, state <= 1, pos - 1) + if (ch < '0' || ch > '9') periodError() var x: Int = '0' - ch while ( (pos < len) && { @@ -963,11 +794,11 @@ private[json] object parsers { x = x * 10 + ('0' - ch) x > 0 } - ) periodError(pos) + ) periodError() pos += 1 } if (!(isNeg ^ isNegX)) { - if (x == -2147483648) periodError(pos) + if (x == -2147483648) periodError() x = -x } if (ch == 'Y' && state <= 0) { @@ -977,15 +808,15 @@ private[json] object parsers { months = x state = 2 } else if (ch == 'W' && state <= 2) { - if (x < -306783378 || x > 306783378) periodError(pos) + if (x < -306783378 || x > 306783378) periodError() days = x * 7 state = 3 } else if (ch == 'D') { val ds = x.toLong + days - if (ds != ds.toInt) periodError(pos) + if (ds != ds.toInt) periodError() days = ds.toInt state = 4 - } else periodError(state, pos) + } else periodError() pos += 1 (pos < len) && { ch = input.charAt(pos) @@ -997,230 +828,175 @@ private[json] object parsers { } def unsafeParseYear(input: String): Year = { - val len = input.length - var pos = 0 - val year = { - if (pos + 3 >= len) yearError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val ch3 = input.charAt(pos + 3) - if (ch0 >= '0' && ch0 <= '9') { - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (len != 4) yearError(pos + 4) - pos += 4 - ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 - } else { - val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) + val len = input.length + var pos, year = 0 + if ( + pos + 3 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) pos += 4 - var year = ch1 * 100 + ch2 * 10 + ch3 - 5328 // 53328 == '0' * 111 - var yearDigits = 3 - var ch: Char = '0' - while ( - pos < len && { - ch = input.charAt(pos) - ch >= '0' && ch <= '9' && yearDigits < 9 + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + pos != len + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && yearError()) + year = ch1 * 100 + ch2 * 10 + ch3 - 5328 // 53328 == '0' * 111 + var yearDigits = 3 + var ch = '0' + while ( + pos < len && { + ch = input.charAt(pos) + ch >= '0' && ch <= '9' && yearDigits < 9 + } + ) { + year = year * 10 + (ch - '0') + yearDigits += 1 + pos += 1 + } + yearNeg && { + year = -year + year == 0 + } || pos != len } - ) { - year = year * 10 + (ch - '0') - yearDigits += 1 - pos += 1 - } - if (yearNeg) { - if (year == 0) yearError(pos - 1) - year = -year - } - if (pos != len || ch < '0' || ch > '9') { - if (yearDigits == 9) yearError(pos) - digitError(pos) } - year } - } + ) yearError() Year.of(year) } def unsafeParseYearMonth(input: String): YearMonth = { - val len = input.length - var pos = 0 - val year = { - if (pos + 4 >= len) yearMonthError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val ch3 = input.charAt(pos + 3) - val ch4 = input.charAt(pos + 4) - if (ch0 >= '0' && ch0 <= '9') { - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 != '-') charError('-', pos + 4) - pos += 5 - ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 - } else { - val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + val len = input.length + var pos, year, month = 0 + if ( + pos + 4 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) pos += 5 - var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 - var yearDigits = 4 - var ch: Char = '0' - while ({ - if (pos >= len) yearMonthError(pos) - ch = input.charAt(pos) - pos += 1 - ch >= '0' && ch <= '9' && yearDigits < 9 - }) { - year = year * 10 + (ch - '0') - yearDigits += 1 - } - if (yearNeg) { - if (year == 0) yearError(pos - 2) - year = -year + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && yearMonthError()) + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ({ + if (pos >= len) yearMonthError() + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + }) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } } - if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) - year + } || pos + 2 != len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || month < 1 || month > 12 } - } - val month = { - if (pos + 1 >= len) yearMonthError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (month < 1 || month > 12) monthError(pos + 1) - pos += 2 - month - } - if (pos != len) yearMonthError(pos) + ) yearMonthError() YearMonth.of(year, month) } def unsafeParseZonedDateTime(input: String): ZonedDateTime = { - val len = input.length - var pos = 0 - val year = { - if (pos + 4 >= len) zonedDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val ch3 = input.charAt(pos + 3) - val ch4 = input.charAt(pos + 4) - if (ch0 >= '0' && ch0 <= '9') { - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 != '-') charError('-', pos + 4) - pos += 5 - ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 - } else { - val yearNeg = ch0 == '-' || (ch0 != '+' && charsOrDigitError('-', '+', pos)) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch2 < '0' || ch2 > '9') digitError(pos + 2) - if (ch3 < '0' || ch3 > '9') digitError(pos + 3) - if (ch4 < '0' || ch4 > '9') digitError(pos + 4) + val len = input.length + var pos, year, month, day, hour, minute = 0 + if ( + pos + 4 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) pos += 5 - var year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 - var yearDigits = 4 - var ch: Char = '0' - while ({ - if (pos >= len) zonedDateTimeError(pos) - ch = input.charAt(pos) - pos += 1 - ch >= '0' && ch <= '9' && yearDigits < 9 - }) { - year = year * 10 + (ch - '0') - yearDigits += 1 - } - if (yearNeg) { - if (year == 0) yearError(pos - 2) - year = -year + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && zonedDateTimeError()) + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ({ + if (pos >= len) zonedDateTimeError() + ch = input.charAt(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + }) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } } - if (ch != '-') yearError(yearNeg, yearDigits, pos - 1) - year - } - } - val month = { - if (pos + 2 >= len) zonedDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (month < 1 || month > 12) monthError(pos + 1) - if (ch2 != '-') charError('-', pos + 2) - pos += 3 - month - } - val day = { - if (pos + 2 >= len) zonedDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val day = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (day == 0 || (day > 28 && day > maxDayForYearMonth(year, month))) dayError(pos + 1) - if (ch2 != 'T') charError('T', pos + 2) - pos += 3 - day - } - val hour = { - if (pos + 2 >= len) zonedDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val ch2 = input.charAt(pos + 2) - val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (hour > 23) hourError(pos + 1) - if (ch2 != ':') charError(':', pos + 2) - pos += 3 - hour - } - val minute = { - if (pos + 1 >= len) zonedDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') minuteError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } - var second, nano = 0 - var nanoDigitWeight = -1 - if (pos >= len) timezoneSignError(nanoDigitWeight, pos) - var ch = input.charAt(pos) - pos += 1 - if (ch == ':') { - nanoDigitWeight = -2 - second = { - if (pos + 1 >= len) zonedDateTimeError(pos) + } || pos + 5 >= len || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') secondError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } - if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + val ch5 = input.charAt(pos + 5) + pos += 6 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || ch5 != 'T' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForYearMonth(year, month)) + } || pos + 4 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + val ch2 = input.charAt(pos + 2) + val ch3 = input.charAt(pos + 3) + val ch4 = input.charAt(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } || pos >= len + ) zonedDateTimeError() + var second, nano = 0 + var ch = input.charAt(pos) + pos += 1 + if (ch == ':') { + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } || pos >= len + ) zonedDateTimeError() ch = input.charAt(pos) pos += 1 if (ch == '.') { - nanoDigitWeight = 100000000 + var nanoDigitWeight = 100000000 while ({ - if (pos >= len) timezoneSignError(nanoDigitWeight, pos) + if (pos >= len) zonedDateTimeError() ch = input.charAt(pos) pos += 1 ch >= '0' && ch <= '9' && nanoDigitWeight != 0 @@ -1235,94 +1011,89 @@ private[json] object parsers { if (ch == 'Z') { if (pos < len) { ch = input.charAt(pos) - if (ch != '[') charError('[', pos) + if (ch != '[') zonedDateTimeError() pos += 1 } ZoneOffset.UTC } else { - val offsetNeg = ch == '-' || (ch != '+' && timezoneSignError(nanoDigitWeight, pos - 1)) - nanoDigitWeight = -3 - val offsetHour = { - if (pos + 1 >= len) zonedDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val offsetHour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (offsetHour > 18) timezoneOffsetHourError(pos + 1) - pos += 2 - offsetHour - } - var offsetMinute, offsetSecond = 0 + val offsetNeg = ch == '-' || (ch != '+' && zonedDateTimeError()) + var offsetTotal = 0 + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' + } + ) zonedDateTimeError() if ( pos < len && { ch = input.charAt(pos) pos += 1 - ch == ':' || ch != '[' && charError('[', pos - 1) + ch == ':' || ch != '[' && zonedDateTimeError() } ) { - offsetMinute = { - if (pos + 1 >= len) zonedDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetMinuteError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) zonedDateTimeError() if ( pos < len && { ch = input.charAt(pos) pos += 1 - ch == ':' || ch != '[' && charError('[', pos - 1) + ch == ':' || ch != '[' && zonedDateTimeError() } ) { - nanoDigitWeight = -4 - offsetSecond = { - if (pos + 1 >= len) zonedDateTimeError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetSecondError(pos + 1) - pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - } + if ( + pos + 1 >= len || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) zonedDateTimeError() if (pos < len) { ch = input.charAt(pos) - if (ch != '[') charError('[', pos) + if (ch != '[') zonedDateTimeError() pos += 1 } } } - toZoneOffset(offsetNeg, offsetHour, offsetMinute, offsetSecond, pos) + if (offsetTotal > 64800) zonedDateTimeError() + toZoneOffset(offsetNeg, offsetTotal) } if (ch == '[') { - val zone = - try { - val from = pos - while ({ - if (pos >= len) zonedDateTimeError(pos) - ch = input.charAt(pos) - ch != ']' - }) pos += 1 - val key = input.substring(from, pos) - var zoneId = zoneIds.get(key) - if ( - (zoneId eq null) && { - zoneId = ZoneId.of(key) - !zoneId.isInstanceOf[ZoneOffset] || zoneId.asInstanceOf[ZoneOffset].getTotalSeconds % 900 == 0 - } - ) zoneIds.put(key, zoneId) - zoneId - } catch { - case _: DateTimeException => zonedDateTimeError(pos - 1) + var zoneId: ZoneId = null + val from = pos + while ({ + if (pos >= len) zonedDateTimeError() + ch = input.charAt(pos) + ch != ']' + }) pos += 1 + val key = input.substring(from, pos) + zoneId = zoneIds.get(key) + if ( + (zoneId eq null) && { + try zoneId = ZoneId.of(key) + catch { + case _: DateTimeException => zonedDateTimeError() + } + !zoneId.isInstanceOf[ZoneOffset] || zoneId.asInstanceOf[ZoneOffset].getTotalSeconds % 900 == 0 } - pos += 1 - if (pos != len) zonedDateTimeError(pos) - ZonedDateTime.ofInstant(localDateTime, zoneOffset, zone) - } else ZonedDateTime.ofLocal(localDateTime, zoneOffset, null) + ) zoneIds.put(key, zoneId) + if (pos + 1 != len) zonedDateTimeError() + ZonedDateTime.ofInstant(localDateTime, zoneOffset, zoneId) + } else { + if (pos != len) zonedDateTimeError() + ZonedDateTime.ofLocal(localDateTime, zoneOffset, null) + } } def unsafeParseZoneId(input: String): ZoneId = @@ -1336,103 +1107,82 @@ private[json] object parsers { ) zoneIds.put(input, zoneId) zoneId } catch { - case _: DateTimeException => error("illegal zone id", 0) + case _: DateTimeException => zoneIdError() } def unsafeParseZoneOffset(input: String): ZoneOffset = { val len = input.length var pos, nanoDigitWeight = 0 - if (pos >= len) zoneOffsetError(pos) - var ch = input.charAt(pos) + if (pos >= len) zoneOffsetError() + val ch = input.charAt(pos) pos += 1 - if (ch == 'Z') ZoneOffset.UTC - else { - val offsetNeg = ch == '-' || (ch != '+' && timezoneSignError(nanoDigitWeight, pos - 1)) + if (ch == 'Z') { + if (pos != len) zoneOffsetError() + ZoneOffset.UTC + } else { + val offsetNeg = ch == '-' || (ch != '+' && zoneOffsetError()) nanoDigitWeight = -3 - val offsetHour = { - if (pos + 1 >= len) zoneOffsetError(pos) - val ch0 = input.charAt(pos) - val ch1 = input.charAt(pos + 1) - val offsetHour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (offsetHour > 18) timezoneOffsetHourError(pos + 1) - pos += 2 - offsetHour - } - var offsetMinute, offsetSecond = 0 + var offsetTotal = 0 if ( - pos < len && { - ch = input.charAt(pos) - pos += 1 - ch == ':' - } - ) { - offsetMinute = { - if (pos + 1 >= len) zoneOffsetError(pos) + pos + 1 >= len || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetMinuteError(pos + 1) + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' } + ) zoneOffsetError() + if (pos < len) { if ( - pos < len && { - ch = input.charAt(pos) + input.charAt(pos) != ':' || { pos += 1 - ch == ':' - } - ) { - nanoDigitWeight = -4 - offsetSecond = { - if (pos + 1 >= len) zoneOffsetError(pos) + pos + 1 >= len + } || { val ch0 = input.charAt(pos) val ch1 = input.charAt(pos + 1) - if (ch0 < '0' || ch0 > '9') digitError(pos) - if (ch1 < '0' || ch1 > '9') digitError(pos + 1) - if (ch0 > '5') timezoneOffsetSecondError(pos + 1) pos += 2 - ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' } + ) zoneOffsetError() + if (pos < len) { + if ( + input.charAt(pos) != ':' || { + pos += 1 + pos + 1 >= len + } || { + val ch0 = input.charAt(pos) + val ch1 = input.charAt(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) zoneOffsetError() } } - if (pos != len) zoneOffsetError(pos) - toZoneOffset(offsetNeg, offsetHour, offsetMinute, offsetSecond, pos) + if (offsetTotal > 64800 || pos != len) zoneOffsetError() // 64800 == 18 * 60 * 60 + toZoneOffset(offsetNeg, offsetTotal) } } - private[this] def toZoneOffset( - offsetNeg: Boolean, - offsetHour: Int, - offsetMinute: Int, - offsetSecond: Int, - pos: Int - ): ZoneOffset = { - var offsetTotal = offsetHour * 3600 + offsetMinute * 60 + offsetSecond - var qp = offsetTotal * 37283 - if (offsetTotal > 64800) zoneOffsetError(pos) // 64800 == 18 * 60 * 60 - if ((qp & 0x1ff8000) == 0) { // check if offsetTotal divisible by 900 - qp >>>= 25 // divide offsetTotal by 900 + private[this] def toZoneOffset(offsetNeg: Boolean, offsetTotal: Int): ZoneOffset = { + var qp = offsetTotal * 37283 + if ((qp & 0x1ff8000) == 0) { // check if offsetTotal divisible by 900 + qp >>>= 25 // divide offsetTotal by 900 if (offsetNeg) qp = -qp var zoneOffset = zoneOffsets(qp + 72) if (zoneOffset ne null) zoneOffset else { - if (offsetNeg) offsetTotal = -offsetTotal - zoneOffset = ZoneOffset.ofTotalSeconds(offsetTotal) + zoneOffset = ZoneOffset.ofTotalSeconds(if (offsetNeg) -offsetTotal else offsetTotal) zoneOffsets(qp + 72) = zoneOffset zoneOffset } - } else { - if (offsetNeg) offsetTotal = -offsetTotal - ZoneOffset.ofTotalSeconds(offsetTotal) - } + } else ZoneOffset.ofTotalSeconds(if (offsetNeg) -offsetTotal else offsetTotal) } - private[this] def sumSeconds(s1: Long, s2: Long, pos: Int): Long = { + private[this] def sumSeconds(s1: Long, s2: Long): Long = { val s = s1 + s2 - if (((s1 ^ s) & (s2 ^ s)) < 0) durationError(pos) + if (((s1 ^ s) & (s2 ^ s)) < 0) durationError() s } @@ -1464,115 +1214,33 @@ private[json] object parsers { ((cp ^ cc) & 0x1fc0000000L) != 0 || (((cp >> 37).toInt - cc) & 0x3) == 0 } - private[this] def nanoError(nanoDigitWeight: Int, ch: Char, pos: Int): Nothing = { - if (nanoDigitWeight == 0) charError(ch, pos) - charOrDigitError(ch, pos) - } - - private[this] def durationOrPeriodStartError(isNeg: Boolean, pos: Int) = - error( - if (isNeg) "expected 'P'" - else "expected 'P' or '-'", - pos - ) - - private[this] def durationOrPeriodDigitError(isNegX: Boolean, isNumReq: Boolean, pos: Int): Nothing = - error( - if (isNegX) "expected digit" - else if (isNumReq) "expected '-' or digit" - else "expected '\"' or '-' or digit", - pos - ) - - private[this] def durationError(state: Int, pos: Int): Nothing = - error( - (state: @switch) match { - case 0 => "expected 'D' or digit" - case 1 => "expected 'H' or 'M' or 'S or '.' or digit" - case 2 => "expected 'M' or 'S or '.' or digit" - case 3 => "expected 'S or '.' or digit" - }, - pos - ) - - private[this] def durationError(pos: Int) = error("illegal duration", pos) - - private[this] def timezoneSignError(nanoDigitWeight: Int, pos: Int) = - error( - if (nanoDigitWeight == -2) "expected '.' or '+' or '-' or 'Z'" - else if (nanoDigitWeight == -1) "expected ':' or '+' or '-' or 'Z'" - else if (nanoDigitWeight == 0) "expected '+' or '-' or 'Z'" - else "expected digit or '+' or '-' or 'Z'", - pos - ) - - private[this] def instantError(pos: Int) = error("illegal instant", pos) - - private[this] def localDateError(pos: Int) = error("illegal local date", pos) - - private[this] def localDateTimeError(pos: Int) = error("illegal local date time", pos) - - private[this] def localTimeError(pos: Int) = error("illegal local time", pos) - - private[this] def offsetDateTimeError(pos: Int) = error("illegal offset date time", pos) - - private[this] def offsetTimeError(pos: Int) = error("illegal offset time", pos) - - private[this] def periodError(state: Int, pos: Int): Nothing = - error( - (state: @switch) match { - case 0 => "expected 'Y' or 'M' or 'W' or 'D' or digit" - case 1 => "expected 'M' or 'W' or 'D' or digit" - case 2 => "expected 'W' or 'D' or digit" - case 3 => "expected 'D' or digit" - }, - pos - ) - - private[this] def periodError(pos: Int) = error("illegal period", pos) - - private[this] def yearMonthError(pos: Int) = error("illegal year month", pos) - - private[this] def zonedDateTimeError(pos: Int) = error("illegal zoned date time", pos) - - private[this] def zoneOffsetError(pos: Int) = error("illegal zone offset", pos) - - private[this] def yearError(yearNeg: Boolean, yearDigits: Int, pos: Int) = { - if (!yearNeg && yearDigits == 4) digitError(pos) - if (yearDigits == 9) charError('-', pos) - charOrDigitError('-', pos) - } - - private[this] def yearError(pos: Int) = error("illegal year", pos) + @noinline private[this] def durationError() = error("expected a Duration") - private[this] def monthError(pos: Int) = error("illegal month", pos) + @noinline private[this] def instantError() = error("expected an Instant") - private[this] def dayError(pos: Int) = error("illegal day", pos) + @noinline private[this] def localDateError() = error("expected a LocalDate") - private[this] def hourError(pos: Int) = error("illegal hour", pos) + @noinline private[this] def localDateTimeError() = error("expected a LocalDateTime") - private[this] def minuteError(pos: Int) = error("illegal minute", pos) + @noinline private[this] def localTimeError() = error("expected a LocalTime") - private[this] def secondError(pos: Int) = error("illegal second", pos) + @noinline private[this] def offsetDateTimeError() = error("expected an OffsetDateTime") - private[this] def timezoneOffsetHourError(pos: Int) = error("illegal timezone offset hour", pos) + @noinline private[this] def offsetTimeError() = error("expected an OffsetTime") - private[this] def timezoneOffsetMinuteError(pos: Int) = error("illegal timezone offset minute", pos) + @noinline private[this] def periodError() = error("expected a Period") - private[this] def timezoneOffsetSecondError(pos: Int) = error("illegal timezone offset second", pos) + @noinline private[this] def monthDayError() = error("expected a MonthDay") - private[this] def digitError(pos: Int) = error("expected digit", pos) + @noinline private[this] def yearMonthError() = error("expected a YearMonth") - private[this] def charsOrDigitError(ch1: Char, ch2: Char, pos: Int) = - error(s"expected '$ch1' or '$ch2' or digit", pos) + @noinline private[this] def zonedDateTimeError() = error("expected a ZonedDateTime") - private[this] def charsError(ch1: Char, ch2: Char, pos: Int) = error(s"expected '$ch1' or '$ch2'", pos) + @noinline private[this] def zoneOffsetError() = error("expected a ZoneOffset") - private[this] def charOrDigitError(ch1: Char, pos: Int) = error(s"expected '$ch1' or digit", pos) + @noinline private[this] def zoneIdError() = error("expected a ZoneId") - private[this] def charError(ch: Char, pos: Int) = error(s"expected '$ch'", pos) + @noinline private[this] def yearError() = error("expected a Year") - @noinline - private[this] def error(msg: String, pos: Int): Nothing = - throw new DateTimeException(msg + " at index " + pos) with NoStackTrace + private[this] def error(msg: String): Nothing = throw new DateTimeException(msg) with NoStackTrace } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 7d04b4b43..c7f62b44b 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -513,15 +513,15 @@ object DecoderSpec extends ZIOSpecDefault { assert(ok3.fromJson[Map[UUID, String]])( isRight(equalTo(expectedMap("00000000-0000-0000-0000-000000000000"))) ) && - assert(bad1.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && - assert(bad2.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && - assert(bad3.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && - assert(bad4.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && - assert(bad5.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && - assert(bad6.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && - assert(bad7.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && - assert(bad8.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) && - assert(bad9.fromJson[Map[UUID, String]])(isLeft(containsString("(expected UUID string)"))) + assert(bad1.fromJson[Map[UUID, String]])(isLeft(containsString("(expected a UUID)"))) && + assert(bad2.fromJson[Map[UUID, String]])(isLeft(containsString("(expected a UUID)"))) && + assert(bad3.fromJson[Map[UUID, String]])(isLeft(containsString("(expected a UUID)"))) && + assert(bad4.fromJson[Map[UUID, String]])(isLeft(containsString("(expected a UUID)"))) && + assert(bad5.fromJson[Map[UUID, String]])(isLeft(containsString("(expected a UUID)"))) && + assert(bad6.fromJson[Map[UUID, String]])(isLeft(containsString("(expected a UUID)"))) && + assert(bad7.fromJson[Map[UUID, String]])(isLeft(containsString("(expected a UUID)"))) && + assert(bad8.fromJson[Map[UUID, String]])(isLeft(containsString("(expected a UUID)"))) && + assert(bad9.fromJson[Map[UUID, String]])(isLeft(containsString("(expected a UUID)"))) }, test("zio.Chunk") { val jsonStr = """["5XL","2XL","XL"]""" @@ -557,15 +557,15 @@ object DecoderSpec extends ZIOSpecDefault { assert(ok1.fromJson[UUID])(isRight(equalTo(UUID.fromString("64d7c38d-2afd-4514-9832-4e70afe4b0f8")))) && assert(ok2.fromJson[UUID])(isRight(equalTo(UUID.fromString("64D7C38D-00FD-0014-0032-0070AfE4B0f8")))) && assert(ok3.fromJson[UUID])(isRight(equalTo(UUID.fromString("00000000-0000-0000-0000-000000000000")))) && - assert(bad1.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad2.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad3.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad4.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad5.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad6.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad7.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad8.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad9.fromJson[UUID])(isLeft(containsString("(expected UUID string)"))) + assert(bad1.fromJson[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad2.fromJson[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad3.fromJson[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad4.fromJson[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad5.fromJson[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad6.fromJson[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad7.fromJson[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad8.fromJson[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad9.fromJson[UUID])(isLeft(containsString("(expected a UUID)"))) }, test("java.util.Currency") { assert(""""USD"""".fromJson[java.util.Currency])(isRight(equalTo(java.util.Currency.getInstance("USD")))) && @@ -579,7 +579,7 @@ object DecoderSpec extends ZIOSpecDefault { assert(ok1.fromJson[Duration])(isRight(equalTo(Duration.parse("PT1H2M3S")))) && assert(ok2.fromJson[Duration])(isRight(equalTo(Duration.ofNanos(-500000000)))) && assert(bad1.fromJson[Duration])( - isLeft(containsString("PT-H is not a valid ISO-8601 format, expected digit at index 3")) + isLeft(containsString("expected a Duration")) ) }, test("java.time.ZonedDateTime") { @@ -594,13 +594,7 @@ object DecoderSpec extends ZIOSpecDefault { assert(ok2.fromJson[ZonedDateTime].map(_.toOffsetDateTime))( isRight(equalTo(OffsetDateTime.parse("2018-10-28T03:30+01:00"))) ) && - assert(bad1.fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2018-10-28T02:30 is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" - ) - ) - ) + assert(bad1.fromJson[ZonedDateTime])(isLeft(equalTo("(expected a ZonedDateTime)"))) }, test("bothWith") { final case class Foo(a: Int) @@ -1074,15 +1068,15 @@ object DecoderSpec extends ZIOSpecDefault { assert(ok1.as[UUID])(isRight(equalTo(UUID.fromString("64d7c38d-2afd-4514-9832-4e70afe4b0f8")))) && assert(ok2.as[UUID])(isRight(equalTo(UUID.fromString("64D7C38D-00FD-0014-0032-0070AFE4B0f8")))) && assert(ok3.as[UUID])(isRight(equalTo(UUID.fromString("00000000-0000-0000-0000-000000000000")))) && - assert(bad1.as[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad2.as[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad3.as[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad4.as[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad5.as[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad6.as[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad7.as[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad8.as[UUID])(isLeft(containsString("(expected UUID string)"))) && - assert(bad9.as[UUID])(isLeft(containsString("(expected UUID string)"))) + assert(bad1.as[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad2.as[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad3.as[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad4.as[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad5.as[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad6.as[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad7.as[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad8.as[UUID])(isLeft(containsString("(expected a UUID)"))) && + assert(bad9.as[UUID])(isLeft(containsString("(expected a UUID)"))) } ) ) diff --git a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala index 6fb1ec273..bcd81eaa5 100644 --- a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala @@ -3,7 +3,6 @@ package zio.json import zio.json.ast._ import zio.test.Assertion._ import zio.test._ - import java.time._ import java.time.format.DateTimeFormatter @@ -247,7 +246,7 @@ object JavaTimeSpec extends ZIOSpecDefault { } ), suite("Decoder")( - test("DayOfWeek") { + test("DayOfWeek fromJson") { assert(stringify("MONDAY").fromJson[DayOfWeek])(isRight(equalTo(DayOfWeek.MONDAY))) && assert(stringify("TUESDAY").fromJson[DayOfWeek])(isRight(equalTo(DayOfWeek.TUESDAY))) && assert(stringify("WEDNESDAY").fromJson[DayOfWeek])(isRight(equalTo(DayOfWeek.WEDNESDAY))) && @@ -256,7 +255,22 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("SATURDAY").fromJson[DayOfWeek])(isRight(equalTo(DayOfWeek.SATURDAY))) && assert(stringify("SUNDAY").fromJson[DayOfWeek])(isRight(equalTo(DayOfWeek.SUNDAY))) && assert(stringify("monday").fromJson[DayOfWeek])(isRight(equalTo(DayOfWeek.MONDAY))) && - assert(stringify("MonDay").fromJson[DayOfWeek])(isRight(equalTo(DayOfWeek.MONDAY))) + assert(stringify("MonDay").fromJson[DayOfWeek])(isRight(equalTo(DayOfWeek.MONDAY))) && + assert(stringify("MonDa\\u0079").fromJson[DayOfWeek])(isRight(equalTo(DayOfWeek.MONDAY))) && + assert(stringify("Mon").fromJson[DayOfWeek])(isLeft(equalTo("(expected a DayOfWeek)"))) + }, + test("DayOfWeek fromJsonAST") { + assert(Json.Str("MONDAY").as[DayOfWeek])(isRight(equalTo(DayOfWeek.MONDAY))) && + assert(Json.Str("TUESDAY").as[DayOfWeek])(isRight(equalTo(DayOfWeek.TUESDAY))) && + assert(Json.Str("WEDNESDAY").as[DayOfWeek])(isRight(equalTo(DayOfWeek.WEDNESDAY))) && + assert(Json.Str("THURSDAY").as[DayOfWeek])(isRight(equalTo(DayOfWeek.THURSDAY))) && + assert(Json.Str("FRIDAY").as[DayOfWeek])(isRight(equalTo(DayOfWeek.FRIDAY))) && + assert(Json.Str("SATURDAY").as[DayOfWeek])(isRight(equalTo(DayOfWeek.SATURDAY))) && + assert(Json.Str("SUNDAY").as[DayOfWeek])(isRight(equalTo(DayOfWeek.SUNDAY))) && + assert(Json.Str("monday").as[DayOfWeek])(isRight(equalTo(DayOfWeek.MONDAY))) && + assert(Json.Str("MonDay").as[DayOfWeek])(isRight(equalTo(DayOfWeek.MONDAY))) && + assert(Json.Str("MonDa\\u0079").as[DayOfWeek])(isLeft(equalTo("(expected a DayOfWeek)"))) && + assert(Json.Str("Mon").as[DayOfWeek])(isLeft(equalTo("(expected a DayOfWeek)"))) }, test("Duration") { assert(stringify("PT24H").fromJson[Duration])(isRight(equalTo(Duration.ofHours(24)))) && @@ -286,6 +300,7 @@ object JavaTimeSpec extends ZIOSpecDefault { val p = LocalDateTime.of(2020, 1, 1, 12, 36, 0) assert(stringify(n).fromJson[LocalDateTime])(isRight(equalTo(n))) && assert(stringify("2020-01-01T12:36").fromJson[LocalDateTime])(isRight(equalTo(p))) && + assert(stringify("2020-01-01T12:36:00").fromJson[LocalDateTime])(isRight(equalTo(p))) && assert(stringify("2020-01-01T12:36:00.").fromJson[LocalDateTime])(isRight(equalTo(p))) }, test("LocalTime") { @@ -293,9 +308,10 @@ object JavaTimeSpec extends ZIOSpecDefault { val p = LocalTime.of(12, 36, 0) assert(stringify(n).fromJson[LocalTime])(isRight(equalTo(n))) && assert(stringify("12:36").fromJson[LocalTime])(isRight(equalTo(p))) && + assert(stringify("12:36:00").fromJson[LocalTime])(isRight(equalTo(p))) && assert(stringify("12:36:00.").fromJson[LocalTime])(isRight(equalTo(p))) }, - test("Month") { + test("Month fromJson") { assert(stringify("JANUARY").fromJson[Month])(isRight(equalTo(Month.JANUARY))) && assert(stringify("FEBRUARY").fromJson[Month])(isRight(equalTo(Month.FEBRUARY))) && assert(stringify("MARCH").fromJson[Month])(isRight(equalTo(Month.MARCH))) && @@ -309,7 +325,27 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("NOVEMBER").fromJson[Month])(isRight(equalTo(Month.NOVEMBER))) && assert(stringify("DECEMBER").fromJson[Month])(isRight(equalTo(Month.DECEMBER))) && assert(stringify("december").fromJson[Month])(isRight(equalTo(Month.DECEMBER))) && - assert(stringify("December").fromJson[Month])(isRight(equalTo(Month.DECEMBER))) + assert(stringify("December").fromJson[Month])(isRight(equalTo(Month.DECEMBER))) && + assert(stringify("Decembe\\u0072").fromJson[Month])(isRight(equalTo(Month.DECEMBER))) && + assert(stringify("Dec").fromJson[Month])(isLeft(equalTo("(expected a Month)"))) + }, + test("Month fromJsonAST") { + assert(Json.Str("JANUARY").as[Month])(isRight(equalTo(Month.JANUARY))) && + assert(Json.Str("FEBRUARY").as[Month])(isRight(equalTo(Month.FEBRUARY))) && + assert(Json.Str("MARCH").as[Month])(isRight(equalTo(Month.MARCH))) && + assert(Json.Str("APRIL").as[Month])(isRight(equalTo(Month.APRIL))) && + assert(Json.Str("MAY").as[Month])(isRight(equalTo(Month.MAY))) && + assert(Json.Str("JUNE").as[Month])(isRight(equalTo(Month.JUNE))) && + assert(Json.Str("JULY").as[Month])(isRight(equalTo(Month.JULY))) && + assert(Json.Str("AUGUST").as[Month])(isRight(equalTo(Month.AUGUST))) && + assert(Json.Str("SEPTEMBER").as[Month])(isRight(equalTo(Month.SEPTEMBER))) && + assert(Json.Str("OCTOBER").as[Month])(isRight(equalTo(Month.OCTOBER))) && + assert(Json.Str("NOVEMBER").as[Month])(isRight(equalTo(Month.NOVEMBER))) && + assert(Json.Str("DECEMBER").as[Month])(isRight(equalTo(Month.DECEMBER))) && + assert(Json.Str("december").as[Month])(isRight(equalTo(Month.DECEMBER))) && + assert(Json.Str("December").as[Month])(isRight(equalTo(Month.DECEMBER))) && + assert(Json.Str("Decembe\\u0072").as[Month])(isLeft(equalTo("(expected a Month)"))) && + assert(Json.Str("Dec").as[Month])(isLeft(equalTo("(expected a Month)"))) }, test("MonthDay") { val n = MonthDay.now() @@ -362,10 +398,15 @@ object JavaTimeSpec extends ZIOSpecDefault { val utc = ZonedDateTime.of(ld, ZoneId.of("Etc/UTC")) val gmt = ZonedDateTime.of(ld, ZoneId.of("+00:00")) + zdtAssert( + "+164433183-11-15T12:32:00.076988677Z[Atlantic/Madeira]", + OffsetDateTime + .parse("+164433183-11-15T12:32:00.076988677Z") + .atZoneSameInstant(ZoneId.of("Atlantic/Madeira")) + ) && zdtAssert(n.toString, n) && zdtAssert("2020-01-01T12:36:00-05:00[America/New_York]", est) && zdtAssert("2020-01-01T12:36:00Z[Etc/UTC]", utc) && - zdtAssert("2020-01-01T12:36:00.Z[Etc/UTC]", utc) && zdtAssert("2020-01-01T12:36:00+00:00[+00:00]", gmt) && zdtAssert( "2018-02-01T00:00Z", @@ -450,2768 +491,1080 @@ object JavaTimeSpec extends ZIOSpecDefault { test("ZoneOffset") { assert(stringify("Z").fromJson[ZoneOffset])(isRight(equalTo(ZoneOffset.UTC))) && assert(stringify("+05:00").fromJson[ZoneOffset])(isRight(equalTo(ZoneOffset.ofHours(5)))) && - assert(stringify("-05:00").fromJson[ZoneOffset])(isRight(equalTo(ZoneOffset.ofHours(-5)))) + assert(stringify("-05:00").fromJson[ZoneOffset])(isRight(equalTo(ZoneOffset.ofHours(-5)))) && + assert(stringify("+05:10:10").fromJson[ZoneOffset])( + isRight(equalTo(ZoneOffset.ofHoursMinutesSeconds(5, 10, 10))) + ) } ), suite("Decoder Sad Path")( - test("DayOfWeek") { - assert(stringify("foody").fromJson[DayOfWeek])( - isLeft(equalTo("(foody is not a valid ISO-8601 format)")) - ) - }, test("Duration") { - assert("""""""".fromJson[Duration])( - isLeft(containsString(" is not a valid ISO-8601 format, illegal duration at index 0")) + assert("""""""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""X"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""-"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""-X"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""PXD"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P-"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P-XD"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1XD"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""PT"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""PT0SX"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P106751991167301D"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1067519911673000D"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P-106751991167301D"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DX1H"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DTXH"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT-XH"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT1XH"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT1H1XM"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT2562047788015216H"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT-2562047788015216H"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT153722867280912931M"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT-153722867280912931M"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT9223372036854775808S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT92233720368547758000S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT-9223372036854775809S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT1H1MXS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT1H1M-XS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT1H1M0XS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT1H1M0.XS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT1H1M0.012345678XS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P1DT1H1M0.0123456789S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT0H0M9223372036854775808S"""".fromJson[Duration])( + isLeft(containsString("expected a Duration")) ) && - assert(""""X"""".fromJson[Duration])( - isLeft(containsString("X is not a valid ISO-8601 format, expected 'P' or '-' at index 0")) + assert(""""P0DT0H0M92233720368547758080S"""".fromJson[Duration])( + isLeft(containsString("expected a Duration")) ) && - assert(""""P"""".fromJson[Duration])( - isLeft(containsString("P is not a valid ISO-8601 format, illegal duration at index 1")) + assert(""""P0DT0H0M-9223372036854775809S"""".fromJson[Duration])( + isLeft(containsString("expected a Duration")) ) && - assert(""""-"""".fromJson[Duration])( - isLeft(containsString("- is not a valid ISO-8601 format, illegal duration at index 1")) + assert(""""P106751991167300DT24H"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT2562047788015215H60M"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(""""P0DT0H153722867280912930M60S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) + }, + test("Instant") { + assert(stringify("").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-0").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-0").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T0").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:0").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("X020-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2X20-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("20X0-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("202X-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020X01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-X1-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-0X-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01X01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-X1T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-0XT01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01X01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01TX1:01").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T0X:01").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T24:01").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01X01").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:X1").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:0X").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:60").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01X").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01:0").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01:X1Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01:0XZ").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01:60Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01:012").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01:01.X").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01:01.123456789X").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""-X"""".fromJson[Duration])( - isLeft(containsString("-X is not a valid ISO-8601 format, expected 'P' at index 1")) + assert(stringify("2020-01-01T01:01:01ZX").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01:01+X1:01:01").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""PXD"""".fromJson[Duration])( - isLeft(containsString("PXD is not a valid ISO-8601 format, expected '-' or digit at index 1")) + assert(stringify("2020-01-01T01:01:01+0").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-01T01:01:01+0X:01:01").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P-"""".fromJson[Duration])( - isLeft(containsString("P- is not a valid ISO-8601 format, illegal duration at index 2")) + assert(stringify("2020-01-01T01:01:01+19:01:01").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P-XD"""".fromJson[Duration])( - isLeft(containsString("P-XD is not a valid ISO-8601 format, expected digit at index 2")) + assert(stringify("2020-01-01T01:01:01+01X01:01").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P1XD"""".fromJson[Duration])( - isLeft(containsString("P1XD is not a valid ISO-8601 format, expected 'D' or digit at index 2")) + assert(stringify("2020-01-01T01:01:01+01:0").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""PT"""".fromJson[Duration])( - isLeft(containsString("PT is not a valid ISO-8601 format, illegal duration at index 2")) + assert(stringify("2020-01-01T01:01:01+01:X1:01").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""PT0SX"""".fromJson[Duration])( - isLeft(containsString("PT0SX is not a valid ISO-8601 format, illegal duration at index 4")) + assert(stringify("2020-01-01T01:01:01+01:0X:01").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P1DT"""".fromJson[Duration])( - isLeft(containsString("P1DT is not a valid ISO-8601 format, illegal duration at index 4")) + assert(stringify("2020-01-01T01:01:01+01:60:01").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P106751991167301D"""".fromJson[Duration])( - isLeft(containsString("P106751991167301D is not a valid ISO-8601 format, illegal duration at index 16")) + assert(stringify("2020-01-01T01:01:01+01:01X01").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P1067519911673000D"""".fromJson[Duration])( - isLeft(containsString("P1067519911673000D is not a valid ISO-8601 format, illegal duration at index 17")) + assert(stringify("2020-01-01T01:01:01+01:01:0").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P-106751991167301D"""".fromJson[Duration])( - isLeft(containsString("P-106751991167301D is not a valid ISO-8601 format, illegal duration at index 17")) + assert(stringify("2020-01-01T01:01:01+01:01:X1").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P1DX1H"""".fromJson[Duration])( - isLeft(containsString("P1DX1H is not a valid ISO-8601 format, expected 'T' or '\"' at index 3")) + assert(stringify("2020-01-01T01:01:01+01:01:0X").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P1DTXH"""".fromJson[Duration])( - isLeft(containsString("P1DTXH is not a valid ISO-8601 format, expected '-' or digit at index 4")) + assert(stringify("2020-01-01T01:01:01+01:01:60").fromJson[Instant])( + isLeft(containsString("expected an Instant")) + ) && + assert(stringify("+X0000-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("+1X000-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("+10X00-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("+100X0-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("+1000X-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("+10000X-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("+100000X-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("+1000000X-01-01T01:01Z").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P1DT-XH"""".fromJson[Duration])( - isLeft(containsString("P1DT-XH is not a valid ISO-8601 format, expected digit at index 5")) + assert(stringify("+1000000001-01-01T01:01Z").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P1DT1XH"""".fromJson[Duration])( - isLeft( - containsString( - "P1DT1XH is not a valid ISO-8601 format, expected 'H' or 'M' or 'S or '.' or digit at index 5" - ) - ) + assert(stringify("+3333333333-01-01T01:01Z").fromJson[Instant])( + isLeft(containsString("expected an Instant")) ) && - assert(""""P1DT1H1XM"""".fromJson[Duration])( - isLeft( - containsString("P1DT1H1XM is not a valid ISO-8601 format, expected 'M' or 'S or '.' or digit at index 7") - ) + assert(stringify("-1000000001-01-01T01:01Z").fromJson[Instant])( + isLeft(containsString("expected an Instant")) + ) && + assert(stringify("-0000-01-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("+10000").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-00-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-13-01T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-00T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-01-32T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-02-30T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-03-32T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-04-31T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-05-32T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-06-31T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-07-32T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-08-32T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-09-31T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-10-32T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-11-31T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && + assert(stringify("2020-12-32T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) + }, + test("LocalDate") { + assert(stringify("").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-0").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-01-0").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-01-012").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("X020-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2X20-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("20X0-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("202X-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020X01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-X1-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-0X-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-01X01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-01-X1").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-01-0X").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+X0000-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+1X000-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+10X00-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+100X0-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+1000X-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+10000X-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+100000X-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+1000000X-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+1000000000-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("-1000000000-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("-0000-01-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("+10000").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-00-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-13-01").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-01-00").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-01-32").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-02-30").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-03-32").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-04-31").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-05-32").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-06-31").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-07-32").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-08-32").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-09-31").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-10-32").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-11-31").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && + assert(stringify("2020-12-32").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) + }, + test("LocalDateTime") { + assert(stringify("").fromJson[LocalDateTime])(isLeft(containsString("expected a LocalDateTime"))) && + assert(stringify("2020").fromJson[LocalDateTime])(isLeft(containsString("expected a LocalDateTime"))) && + assert(stringify("2020-0").fromJson[LocalDateTime])(isLeft(containsString("expected a LocalDateTime"))) && + assert(stringify("2020-01-0").fromJson[LocalDateTime])(isLeft(containsString("expected a LocalDateTime"))) && + assert(stringify("2020-01-01T0").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT2562047788015216H"""".fromJson[Duration])( - isLeft(containsString("P0DT2562047788015216H is not a valid ISO-8601 format, illegal duration at index 20")) + assert(stringify("2020-01-01T01:0").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT-2562047788015216H"""".fromJson[Duration])( - isLeft( - containsString("P0DT-2562047788015216H is not a valid ISO-8601 format, illegal duration at index 21") - ) + assert(stringify("X020-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT153722867280912931M"""".fromJson[Duration])( - isLeft( - containsString("P0DT153722867280912931M is not a valid ISO-8601 format, illegal duration at index 22") - ) + assert(stringify("2X20-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT-153722867280912931M"""".fromJson[Duration])( - isLeft( - containsString("P0DT-153722867280912931M is not a valid ISO-8601 format, illegal duration at index 23") - ) + assert(stringify("20X0-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT9223372036854775808S"""".fromJson[Duration])( - isLeft( - containsString("P0DT9223372036854775808S is not a valid ISO-8601 format, illegal duration at index 23") - ) + assert(stringify("202X-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT92233720368547758000S"""".fromJson[Duration])( - isLeft( - containsString("P0DT92233720368547758000S is not a valid ISO-8601 format, illegal duration at index 23") - ) + assert(stringify("2020X01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT-9223372036854775809S"""".fromJson[Duration])( - isLeft( - containsString("P0DT-9223372036854775809S is not a valid ISO-8601 format, illegal duration at index 23") - ) + assert(stringify("2020-X1-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P1DT1H1MXS"""".fromJson[Duration])( - isLeft( - containsString("P1DT1H1MXS is not a valid ISO-8601 format, expected '\"' or '-' or digit at index 8") - ) + assert(stringify("2020-0X-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P1DT1H1M-XS"""".fromJson[Duration])( - isLeft(containsString("P1DT1H1M-XS is not a valid ISO-8601 format, expected digit at index 9")) + assert(stringify("2020-01X01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P1DT1H1M0XS"""".fromJson[Duration])( - isLeft(containsString("P1DT1H1M0XS is not a valid ISO-8601 format, expected 'S or '.' or digit at index 9")) + assert(stringify("2020-01-X1T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P1DT1H1M0.XS"""".fromJson[Duration])( - isLeft(containsString("P1DT1H1M0.XS is not a valid ISO-8601 format, expected 'S' or digit at index 10")) + assert(stringify("2020-01-0XT01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P1DT1H1M0.012345678XS"""".fromJson[Duration])( - isLeft(containsString("P1DT1H1M0.012345678XS is not a valid ISO-8601 format, expected 'S' at index 19")) + assert(stringify("2020-01-01X01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P1DT1H1M0.0123456789S"""".fromJson[Duration])( - isLeft(containsString("P1DT1H1M0.0123456789S is not a valid ISO-8601 format, expected 'S' at index 19")) + assert(stringify("2020-01-01TX1:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT0H0M9223372036854775808S"""".fromJson[Duration])( - isLeft( - containsString( - "P0DT0H0M9223372036854775808S is not a valid ISO-8601 format, illegal duration at index 27" - ) - ) + assert(stringify("2020-01-01T0X:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT0H0M92233720368547758080S"""".fromJson[Duration])( - isLeft( - containsString( - "P0DT0H0M92233720368547758080S is not a valid ISO-8601 format, illegal duration at index 27" - ) - ) + assert(stringify("2020-01-01T24:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT0H0M-9223372036854775809S"""".fromJson[Duration])( - isLeft( - containsString( - "P0DT0H0M-9223372036854775809S is not a valid ISO-8601 format, illegal duration at index 27" - ) - ) + assert(stringify("2020-01-01T01X01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P106751991167300DT24H"""".fromJson[Duration])( - isLeft(containsString("P106751991167300DT24H is not a valid ISO-8601 format, illegal duration at index 20")) + assert(stringify("2020-01-01T01:X1").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT2562047788015215H60M"""".fromJson[Duration])( - isLeft( - containsString("P0DT2562047788015215H60M is not a valid ISO-8601 format, illegal duration at index 23") - ) + assert(stringify("2020-01-01T01:0X").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(""""P0DT0H153722867280912930M60S"""".fromJson[Duration])( - isLeft( - containsString( - "P0DT0H153722867280912930M60S is not a valid ISO-8601 format, illegal duration at index 27" - ) - ) - ) - }, - test("Instant") { - assert(stringify("").fromJson[Instant])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal instant at index 0)") - ) + assert(stringify("2020-01-01T01:60").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020").fromJson[Instant])( - isLeft( - equalTo("(2020 is not a valid ISO-8601 format, illegal instant at index 0)") - ) + assert(stringify("2020-01-01T01:01X").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-0").fromJson[Instant])( - isLeft( - equalTo("(2020-0 is not a valid ISO-8601 format, illegal instant at index 5)") - ) + assert(stringify("2020-01-01T01:01:0").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-0").fromJson[Instant])( - isLeft( - equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal instant at index 8)") - ) + assert(stringify("2020-01-01T01:01:X1").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T0").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T0 is not a valid ISO-8601 format, illegal instant at index 11)") - ) + assert(stringify("2020-01-01T01:01:0X").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:0").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:0 is not a valid ISO-8601 format, illegal instant at index 14)") - ) + assert(stringify("2020-01-01T01:01:60").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("X020-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(X020-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") - ) + assert(stringify("2020-01-01T01:01:012").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2X20-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2X20-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") - ) + assert(stringify("2020-01-01T01:01:01.X").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("20X0-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(20X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") - ) + assert(stringify("+X0000-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("202X-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(202X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") - ) + assert(stringify("+1X000-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020X01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020X01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 4)") - ) + assert(stringify("+10X00-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-X1-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-X1-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") - ) + assert(stringify("+100X0-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-0X-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-0X-01T01:01Z is not a valid ISO-8601 format, expected digit at index 6)") - ) + assert(stringify("+1000X-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01X01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-01X01T01:01Z is not a valid ISO-8601 format, expected '-' at index 7)") - ) + assert(stringify("+10000X-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-X1T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-01-X1T01:01Z is not a valid ISO-8601 format, expected digit at index 8)") - ) + assert(stringify("+100000X-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-0XT01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-01-0XT01:01Z is not a valid ISO-8601 format, expected digit at index 9)") - ) + assert(stringify("+1000000X-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01X01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01X01:01Z is not a valid ISO-8601 format, expected 'T' at index 10)") - ) + assert(stringify("+1000000000-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01TX1:01").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01TX1:01 is not a valid ISO-8601 format, expected digit at index 11)") - ) + assert(stringify("-1000000000-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T0X:01").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T0X:01 is not a valid ISO-8601 format, expected digit at index 12)") - ) + assert(stringify("-0000-01-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T24:01").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T24:01 is not a valid ISO-8601 format, illegal hour at index 12)") - ) + assert(stringify("+10000").fromJson[LocalDateTime])(isLeft(containsString("expected a LocalDateTime"))) && + assert(stringify("2020-00-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01X01").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01X01 is not a valid ISO-8601 format, expected ':' at index 13)") - ) + assert(stringify("2020-13-01T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:X1").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:X1 is not a valid ISO-8601 format, expected digit at index 14)") - ) + assert(stringify("2020-01-00T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:0X").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:0X is not a valid ISO-8601 format, expected digit at index 15)") - ) + assert(stringify("2020-01-32T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:60").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:60 is not a valid ISO-8601 format, illegal minute at index 15)") - ) + assert(stringify("2020-02-30T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01X").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01X is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" - ) - ) + assert(stringify("2020-03-32T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01:0").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:0 is not a valid ISO-8601 format, illegal instant at index 17)") - ) + assert(stringify("2020-04-31T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01:X1Z").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:X1Z is not a valid ISO-8601 format, expected digit at index 17)") - ) + assert(stringify("2020-05-32T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01:0XZ").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:0XZ is not a valid ISO-8601 format, expected digit at index 18)") - ) + assert(stringify("2020-06-31T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01:60Z").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:60Z is not a valid ISO-8601 format, illegal second at index 18)") - ) + assert(stringify("2020-07-32T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01:012").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01:012 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 19)" - ) - ) + assert(stringify("2020-08-32T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01:01.X").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01:01.X is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 20)" - ) - ) + assert(stringify("2020-09-31T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01:01.123456789X").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01:01.123456789X is not a valid ISO-8601 format, expected '+' or '-' or 'Z' at index 29)" - ) - ) + assert(stringify("2020-10-32T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01:01ZX").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:01ZX is not a valid ISO-8601 format, illegal instant at index 20)") - ) + assert(stringify("2020-11-31T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+X1:01:01").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:01+X1:01:01 is not a valid ISO-8601 format, expected digit at index 20)") - ) + assert(stringify("2020-12-32T01:01").fromJson[LocalDateTime])( + isLeft(containsString("expected a LocalDateTime")) + ) + }, + test("LocalTime") { + assert(stringify("").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("0").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:0").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("X1:01").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("0X:01").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("24:01").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01X01").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:X1").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:0X").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:60").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:01X").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:01:0").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:01:X1").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:01:0X").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:01:60").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:01:012").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && + assert(stringify("01:01:01.X").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) + }, + test("MonthDay") { + assert(stringify("").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("X-01-01").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("-X01-01").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--X1-01").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--0X-01").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--00-01").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--13-01").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--01X01").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--01-X1").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--01-0X").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--01-00").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--01-32").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--02-30").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--03-32").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--04-31").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--05-32").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--06-31").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--07-32").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--08-32").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--09-31").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--10-32").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--11-31").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && + assert(stringify("--12-32").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) + }, + test("OffsetDateTime") { + assert(stringify("").fromJson[OffsetDateTime])(isLeft(containsString("expected an OffsetDateTime"))) && + assert(stringify("2020").fromJson[OffsetDateTime])(isLeft(containsString("expected an OffsetDateTime"))) && + assert(stringify("2020-0").fromJson[OffsetDateTime])(isLeft(containsString("expected an OffsetDateTime"))) && + assert(stringify("2020-01-0").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+0").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:01+0 is not a valid ISO-8601 format, illegal instant at index 20)") - ) + assert(stringify("2020-01-01T0").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+0X:01:01").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:01+0X:01:01 is not a valid ISO-8601 format, expected digit at index 21)") - ) + assert(stringify("2020-01-01T01:0").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+19:01:01").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+19:01:01 is not a valid ISO-8601 format, illegal timezone offset hour at index 21)" - ) - ) + assert(stringify("X020-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01X01:01").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01X01:01 is not a valid ISO-8601 format, illegal instant at index 23)" - ) - ) + assert(stringify("2X20-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01:0").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:0 is not a valid ISO-8601 format, illegal instant at index 23)") - ) + assert(stringify("20X0-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01:X1:01").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:X1:01 is not a valid ISO-8601 format, expected digit at index 23)") - ) + assert(stringify("202X-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01:0X:01").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:0X:01 is not a valid ISO-8601 format, expected digit at index 24)") - ) + assert(stringify("2020X01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01:60:01").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:60:01 is not a valid ISO-8601 format, illegal timezone offset minute at index 24)" - ) - ) + assert(stringify("2020-X1-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01:01X01").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:01X01 is not a valid ISO-8601 format, illegal instant at index 26)" - ) - ) + assert(stringify("2020-0X-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01:01:0").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:01:0 is not a valid ISO-8601 format, illegal instant at index 26)" - ) - ) + assert(stringify("2020-01X01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01:01:X1").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:01:X1 is not a valid ISO-8601 format, expected digit at index 26)") - ) + assert(stringify("2020-01-X1T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01:01:0X").fromJson[Instant])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:01:0X is not a valid ISO-8601 format, expected digit at index 27)") - ) + assert(stringify("2020-01-0XT01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-01T01:01:01+01:01:60").fromJson[Instant])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:01:60 is not a valid ISO-8601 format, illegal timezone offset second at index 27)" - ) - ) + assert(stringify("2020-01-01X01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+X0000-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+X0000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") - ) + assert(stringify("2020-01-01TX1:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1X000-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+1X000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") - ) + assert(stringify("2020-01-01T0X:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+10X00-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+10X00-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") - ) + assert(stringify("2020-01-01T24:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+100X0-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+100X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 4)") - ) + assert(stringify("2020-01-01T01X").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1000X-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+1000X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") - ) + assert(stringify("2020-01-01T01X01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+10000X-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+10000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 6)") - ) + assert(stringify("2020-01-01T01:X1").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+100000X-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+100000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 7)") - ) + assert(stringify("2020-01-01T01:0X").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1000000X-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+1000000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 8)") - ) + assert(stringify("2020-01-01T01:60").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1000000001-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+1000000001-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 10)") - ) + assert(stringify("2020-01-01T01:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+3333333333-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(+3333333333-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 10)") - ) + assert(stringify("2020-01-01T01:01X").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("-1000000001-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(-1000000001-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 10)") - ) + assert(stringify("2020-01-01T01:01:0").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("-0000-01-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(-0000-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 4)") - ) + assert(stringify("2020-01-01T01:01:X1Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+10000").fromJson[Instant])( - isLeft( - equalTo("(+10000 is not a valid ISO-8601 format, illegal instant at index 6)") - ) + assert(stringify("2020-01-01T01:01:0XZ").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-00-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-00-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") - ) + assert(stringify("2020-01-01T01:01:60Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-13-01T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-13-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") - ) + assert(stringify("2020-01-01T01:01:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-00T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-01-00T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + assert(stringify("2020-01-01T01:01:012").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-01-32T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-01-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + assert(stringify("2020-01-01T01:01:01.").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-02-30T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-02-30T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + assert(stringify("2020-01-01T01:01:01.X").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-03-32T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-03-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + assert(stringify("2020-01-01T01:01:01.123456789X").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-04-31T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-04-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-05-32T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-05-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-06-31T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-06-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-07-32T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-07-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-08-32T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-08-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-09-31T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-09-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-10-32T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-10-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-11-31T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-11-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-12-32T01:01Z").fromJson[Instant])( - isLeft( - equalTo("(2020-12-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) - }, - test("LocalDate") { - assert(stringify("").fromJson[LocalDate])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal local date at index 0)") - ) - ) && - assert(stringify("2020").fromJson[LocalDate])( - isLeft( - equalTo("(2020 is not a valid ISO-8601 format, illegal local date at index 0)") - ) - ) && - assert(stringify("2020-0").fromJson[LocalDate])( - isLeft( - equalTo("(2020-0 is not a valid ISO-8601 format, illegal local date at index 5)") - ) - ) && - assert(stringify("2020-01-0").fromJson[LocalDate])( - isLeft( - equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal local date at index 8)") - ) - ) && - assert(stringify("2020-01-012").fromJson[LocalDate])( - isLeft( - equalTo("(2020-01-012 is not a valid ISO-8601 format, illegal local date at index 10)") - ) - ) && - assert(stringify("X020-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(X020-01-01 is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") - ) - ) && - assert(stringify("2X20-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(2X20-01-01 is not a valid ISO-8601 format, expected digit at index 1)") - ) - ) && - assert(stringify("20X0-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(20X0-01-01 is not a valid ISO-8601 format, expected digit at index 2)") - ) - ) && - assert(stringify("202X-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(202X-01-01 is not a valid ISO-8601 format, expected digit at index 3)") - ) - ) && - assert(stringify("2020X01-01").fromJson[LocalDate])( - isLeft( - equalTo("(2020X01-01 is not a valid ISO-8601 format, expected '-' at index 4)") - ) - ) && - assert(stringify("2020-X1-01").fromJson[LocalDate])( - isLeft( - equalTo("(2020-X1-01 is not a valid ISO-8601 format, expected digit at index 5)") - ) - ) && - assert(stringify("2020-0X-01").fromJson[LocalDate])( - isLeft( - equalTo("(2020-0X-01 is not a valid ISO-8601 format, expected digit at index 6)") - ) - ) && - assert(stringify("2020-01X01").fromJson[LocalDate])( - isLeft( - equalTo("(2020-01X01 is not a valid ISO-8601 format, expected '-' at index 7)") - ) - ) && - assert(stringify("2020-01-X1").fromJson[LocalDate])( - isLeft( - equalTo("(2020-01-X1 is not a valid ISO-8601 format, expected digit at index 8)") - ) - ) && - assert(stringify("2020-01-0X").fromJson[LocalDate])( - isLeft( - equalTo("(2020-01-0X is not a valid ISO-8601 format, expected digit at index 9)") - ) - ) && - assert(stringify("+X0000-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(+X0000-01-01 is not a valid ISO-8601 format, expected digit at index 1)") - ) - ) && - assert(stringify("+1X000-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(+1X000-01-01 is not a valid ISO-8601 format, expected digit at index 2)") - ) - ) && - assert(stringify("+10X00-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(+10X00-01-01 is not a valid ISO-8601 format, expected digit at index 3)") - ) - ) && - assert(stringify("+100X0-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(+100X0-01-01 is not a valid ISO-8601 format, expected digit at index 4)") - ) - ) && - assert(stringify("+1000X-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(+1000X-01-01 is not a valid ISO-8601 format, expected digit at index 5)") - ) - ) && - assert(stringify("+10000X-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(+10000X-01-01 is not a valid ISO-8601 format, expected '-' or digit at index 6)") - ) - ) && - assert(stringify("+100000X-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(+100000X-01-01 is not a valid ISO-8601 format, expected '-' or digit at index 7)") - ) - ) && - assert(stringify("+1000000X-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(+1000000X-01-01 is not a valid ISO-8601 format, expected '-' or digit at index 8)") - ) - ) && - assert(stringify("+1000000000-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(+1000000000-01-01 is not a valid ISO-8601 format, expected '-' at index 10)") - ) - ) && - assert(stringify("-1000000000-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(-1000000000-01-01 is not a valid ISO-8601 format, expected '-' at index 10)") - ) - ) && - assert(stringify("-0000-01-01").fromJson[LocalDate])( - isLeft( - equalTo("(-0000-01-01 is not a valid ISO-8601 format, illegal year at index 4)") - ) - ) && - assert(stringify("+10000").fromJson[LocalDate])( - isLeft( - equalTo("(+10000 is not a valid ISO-8601 format, illegal local date at index 6)") - ) - ) && - assert(stringify("2020-00-01").fromJson[LocalDate])( - isLeft( - equalTo("(2020-00-01 is not a valid ISO-8601 format, illegal month at index 6)") - ) - ) && - assert(stringify("2020-13-01").fromJson[LocalDate])( - isLeft( - equalTo("(2020-13-01 is not a valid ISO-8601 format, illegal month at index 6)") - ) - ) && - assert(stringify("2020-01-00").fromJson[LocalDate])( - isLeft( - equalTo("(2020-01-00 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-01-32").fromJson[LocalDate])( - isLeft( - equalTo("(2020-01-32 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-02-30").fromJson[LocalDate])( - isLeft( - equalTo("(2020-02-30 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-03-32").fromJson[LocalDate])( - isLeft( - equalTo("(2020-03-32 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-04-31").fromJson[LocalDate])( - isLeft( - equalTo("(2020-04-31 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-05-32").fromJson[LocalDate])( - isLeft( - equalTo("(2020-05-32 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-06-31").fromJson[LocalDate])( - isLeft( - equalTo("(2020-06-31 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-07-32").fromJson[LocalDate])( - isLeft( - equalTo("(2020-07-32 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-08-32").fromJson[LocalDate])( - isLeft( - equalTo("(2020-08-32 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-09-31").fromJson[LocalDate])( - isLeft( - equalTo("(2020-09-31 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-10-32").fromJson[LocalDate])( - isLeft( - equalTo("(2020-10-32 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-11-31").fromJson[LocalDate])( - isLeft( - equalTo("(2020-11-31 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-12-32").fromJson[LocalDate])( - isLeft( - equalTo("(2020-12-32 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) - }, - test("LocalDateTime") { - assert(stringify("").fromJson[LocalDateTime])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal local date time at index 0)") - ) - ) && - assert(stringify("2020").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020 is not a valid ISO-8601 format, illegal local date time at index 0)") - ) - ) && - assert(stringify("2020-0").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-0 is not a valid ISO-8601 format, illegal local date time at index 5)") - ) - ) && - assert(stringify("2020-01-0").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal local date time at index 8)") - ) - ) && - assert(stringify("2020-01-01T0").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T0 is not a valid ISO-8601 format, illegal local date time at index 11)") - ) - ) && - assert(stringify("2020-01-01T01:0").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:0 is not a valid ISO-8601 format, illegal local date time at index 14)") - ) - ) && - assert(stringify("X020-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(X020-01-01T01:01 is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") - ) - ) && - assert(stringify("2X20-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2X20-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 1)") - ) - ) && - assert(stringify("20X0-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(20X0-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 2)") - ) - ) && - assert(stringify("202X-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(202X-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 3)") - ) - ) && - assert(stringify("2020X01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020X01-01T01:01 is not a valid ISO-8601 format, expected '-' at index 4)") - ) - ) && - assert(stringify("2020-X1-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-X1-01T01:01 is not a valid ISO-8601 format, expected digit at index 5)") - ) - ) && - assert(stringify("2020-0X-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-0X-01T01:01 is not a valid ISO-8601 format, expected digit at index 6)") - ) - ) && - assert(stringify("2020-01X01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01X01T01:01 is not a valid ISO-8601 format, expected '-' at index 7)") - ) - ) && - assert(stringify("2020-01-X1T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-X1T01:01 is not a valid ISO-8601 format, expected digit at index 8)") - ) - ) && - assert(stringify("2020-01-0XT01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-0XT01:01 is not a valid ISO-8601 format, expected digit at index 9)") - ) - ) && - assert(stringify("2020-01-01X01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01X01:01 is not a valid ISO-8601 format, expected 'T' at index 10)") - ) - ) && - assert(stringify("2020-01-01TX1:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01TX1:01 is not a valid ISO-8601 format, expected digit at index 11)") - ) - ) && - assert(stringify("2020-01-01T0X:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T0X:01 is not a valid ISO-8601 format, expected digit at index 12)") - ) - ) && - assert(stringify("2020-01-01T24:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T24:01 is not a valid ISO-8601 format, illegal hour at index 12)") - ) - ) && - assert(stringify("2020-01-01T01X01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01X01 is not a valid ISO-8601 format, expected ':' at index 13)") - ) - ) && - assert(stringify("2020-01-01T01:X1").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:X1 is not a valid ISO-8601 format, expected digit at index 14)") - ) - ) && - assert(stringify("2020-01-01T01:0X").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:0X is not a valid ISO-8601 format, expected digit at index 15)") - ) - ) && - assert(stringify("2020-01-01T01:60").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:60 is not a valid ISO-8601 format, illegal minute at index 15)") - ) - ) && - assert(stringify("2020-01-01T01:01X").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:01X is not a valid ISO-8601 format, expected ':' at index 16)") - ) - ) && - assert(stringify("2020-01-01T01:01:0").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:0 is not a valid ISO-8601 format, illegal local date time at index 17)") - ) - ) && - assert(stringify("2020-01-01T01:01:X1").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:X1 is not a valid ISO-8601 format, expected digit at index 17)") - ) - ) && - assert(stringify("2020-01-01T01:01:0X").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:0X is not a valid ISO-8601 format, expected digit at index 18)") - ) - ) && - assert(stringify("2020-01-01T01:01:60").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:60 is not a valid ISO-8601 format, illegal second at index 18)") - ) - ) && - assert(stringify("2020-01-01T01:01:012").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:012 is not a valid ISO-8601 format, expected '.' at index 19)") - ) - ) && - assert(stringify("2020-01-01T01:01:01.X").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01.X is not a valid ISO-8601 format, illegal local date time at index 20)") - ) - ) && - assert(stringify("+X0000-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(+X0000-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 1)") - ) - ) && - assert(stringify("+1X000-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(+1X000-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 2)") - ) - ) && - assert(stringify("+10X00-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(+10X00-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 3)") - ) - ) && - assert(stringify("+100X0-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(+100X0-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 4)") - ) - ) && - assert(stringify("+1000X-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(+1000X-01-01T01:01 is not a valid ISO-8601 format, expected digit at index 5)") - ) - ) && - assert(stringify("+10000X-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(+10000X-01-01T01:01 is not a valid ISO-8601 format, expected '-' or digit at index 6)") - ) - ) && - assert(stringify("+100000X-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(+100000X-01-01T01:01 is not a valid ISO-8601 format, expected '-' or digit at index 7)") - ) - ) && - assert(stringify("+1000000X-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(+1000000X-01-01T01:01 is not a valid ISO-8601 format, expected '-' or digit at index 8)") - ) - ) && - assert(stringify("+1000000000-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(+1000000000-01-01T01:01 is not a valid ISO-8601 format, expected '-' at index 10)") - ) - ) && - assert(stringify("-1000000000-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(-1000000000-01-01T01:01 is not a valid ISO-8601 format, expected '-' at index 10)") - ) - ) && - assert(stringify("-0000-01-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(-0000-01-01T01:01 is not a valid ISO-8601 format, illegal year at index 4)") - ) - ) && - assert(stringify("+10000").fromJson[LocalDateTime])( - isLeft( - equalTo("(+10000 is not a valid ISO-8601 format, illegal local date time at index 6)") - ) - ) && - assert(stringify("2020-00-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-00-01T01:01 is not a valid ISO-8601 format, illegal month at index 6)") - ) - ) && - assert(stringify("2020-13-01T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-13-01T01:01 is not a valid ISO-8601 format, illegal month at index 6)") - ) - ) && - assert(stringify("2020-01-00T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-00T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-01-32T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-01-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-02-30T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-02-30T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-03-32T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-03-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-04-31T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-04-31T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-05-32T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-05-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-06-31T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-06-31T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-07-32T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-07-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-08-32T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-08-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-09-31T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-09-31T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-10-32T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-10-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-11-31T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-11-31T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-12-32T01:01").fromJson[LocalDateTime])( - isLeft( - equalTo("(2020-12-32T01:01 is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) - }, - test("LocalTime") { - assert(stringify("").fromJson[LocalTime])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal local time at index 0)") - ) - ) && - assert(stringify("0").fromJson[LocalTime])( - isLeft( - equalTo("(0 is not a valid ISO-8601 format, illegal local time at index 0)") - ) - ) && - assert(stringify("01:0").fromJson[LocalTime])( - isLeft( - equalTo("(01:0 is not a valid ISO-8601 format, illegal local time at index 3)") - ) - ) && - assert(stringify("X1:01").fromJson[LocalTime])( - isLeft( - equalTo("(X1:01 is not a valid ISO-8601 format, expected digit at index 0)") - ) - ) && - assert(stringify("0X:01").fromJson[LocalTime])( - isLeft( - equalTo("(0X:01 is not a valid ISO-8601 format, expected digit at index 1)") - ) - ) && - assert(stringify("24:01").fromJson[LocalTime])( - isLeft( - equalTo("(24:01 is not a valid ISO-8601 format, illegal hour at index 1)") - ) - ) && - assert(stringify("01X01").fromJson[LocalTime])( - isLeft( - equalTo("(01X01 is not a valid ISO-8601 format, expected ':' at index 2)") - ) - ) && - assert(stringify("01:X1").fromJson[LocalTime])( - isLeft( - equalTo("(01:X1 is not a valid ISO-8601 format, expected digit at index 3)") - ) - ) && - assert(stringify("01:0X").fromJson[LocalTime])( - isLeft( - equalTo("(01:0X is not a valid ISO-8601 format, expected digit at index 4)") - ) - ) && - assert(stringify("01:60").fromJson[LocalTime])( - isLeft( - equalTo("(01:60 is not a valid ISO-8601 format, illegal minute at index 4)") - ) - ) && - assert(stringify("01:01X").fromJson[LocalTime])( - isLeft( - equalTo("(01:01X is not a valid ISO-8601 format, expected ':' at index 5)") - ) - ) && - assert(stringify("01:01:0").fromJson[LocalTime])( - isLeft( - equalTo("(01:01:0 is not a valid ISO-8601 format, illegal local time at index 6)") - ) - ) && - assert(stringify("01:01:X1").fromJson[LocalTime])( - isLeft( - equalTo("(01:01:X1 is not a valid ISO-8601 format, expected digit at index 6)") - ) - ) && - assert(stringify("01:01:0X").fromJson[LocalTime])( - isLeft( - equalTo("(01:01:0X is not a valid ISO-8601 format, expected digit at index 7)") - ) - ) && - assert(stringify("01:01:60").fromJson[LocalTime])( - isLeft( - equalTo("(01:01:60 is not a valid ISO-8601 format, illegal second at index 7)") - ) - ) && - assert(stringify("01:01:012").fromJson[LocalTime])( - isLeft( - equalTo("(01:01:012 is not a valid ISO-8601 format, expected '.' at index 8)") - ) - ) && - assert(stringify("01:01:01.X").fromJson[LocalTime])( - isLeft( - equalTo("(01:01:01.X is not a valid ISO-8601 format, illegal local time at index 9)") - ) - ) - }, - test("Month") { - assert(stringify("FebTober").fromJson[Month])( - isLeft(equalTo("(FebTober is not a valid ISO-8601 format)")) - ) - }, - test("MonthDay") { - assert(stringify("").fromJson[MonthDay])( - isLeft(equalTo("( is not a valid ISO-8601 format, illegal month day at index 0)")) - ) && - assert(stringify("X-01-01").fromJson[MonthDay])( - isLeft(equalTo("(X-01-01 is not a valid ISO-8601 format, expected '-' at index 0)")) - ) && - assert(stringify("-X01-01").fromJson[MonthDay])( - isLeft(equalTo("(-X01-01 is not a valid ISO-8601 format, expected '-' at index 1)")) - ) && - assert(stringify("--X1-01").fromJson[MonthDay])( - isLeft(equalTo("(--X1-01 is not a valid ISO-8601 format, expected digit at index 2)")) - ) && - assert(stringify("--0X-01").fromJson[MonthDay])( - isLeft(equalTo("(--0X-01 is not a valid ISO-8601 format, expected digit at index 3)")) - ) && - assert(stringify("--00-01").fromJson[MonthDay])( - isLeft(equalTo("(--00-01 is not a valid ISO-8601 format, illegal month at index 3)")) - ) && - assert(stringify("--13-01").fromJson[MonthDay])( - isLeft(equalTo("(--13-01 is not a valid ISO-8601 format, illegal month at index 3)")) - ) && - assert(stringify("--01X01").fromJson[MonthDay])( - isLeft(equalTo("(--01X01 is not a valid ISO-8601 format, expected '-' at index 4)")) - ) && - assert(stringify("--01-X1").fromJson[MonthDay])( - isLeft(equalTo("(--01-X1 is not a valid ISO-8601 format, expected digit at index 5)")) - ) && - assert(stringify("--01-0X").fromJson[MonthDay])( - isLeft(equalTo("(--01-0X is not a valid ISO-8601 format, expected digit at index 6)")) - ) && - assert(stringify("--01-00").fromJson[MonthDay])( - isLeft(equalTo("(--01-00 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--01-32").fromJson[MonthDay])( - isLeft(equalTo("(--01-32 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--02-30").fromJson[MonthDay])( - isLeft(equalTo("(--02-30 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--03-32").fromJson[MonthDay])( - isLeft(equalTo("(--03-32 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--04-31").fromJson[MonthDay])( - isLeft(equalTo("(--04-31 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--05-32").fromJson[MonthDay])( - isLeft(equalTo("(--05-32 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--06-31").fromJson[MonthDay])( - isLeft(equalTo("(--06-31 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--07-32").fromJson[MonthDay])( - isLeft(equalTo("(--07-32 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--08-32").fromJson[MonthDay])( - isLeft(equalTo("(--08-32 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--09-31").fromJson[MonthDay])( - isLeft(equalTo("(--09-31 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--10-32").fromJson[MonthDay])( - isLeft(equalTo("(--10-32 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--11-31").fromJson[MonthDay])( - isLeft(equalTo("(--11-31 is not a valid ISO-8601 format, illegal day at index 6)")) - ) && - assert(stringify("--12-32").fromJson[MonthDay])( - isLeft(equalTo("(--12-32 is not a valid ISO-8601 format, illegal day at index 6)")) - ) - }, - test("OffsetDateTime") { - assert(stringify("").fromJson[OffsetDateTime])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal offset date time at index 0)") - ) - ) && - assert(stringify("2020").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020 is not a valid ISO-8601 format, illegal offset date time at index 0)") - ) - ) && - assert(stringify("2020-0").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-0 is not a valid ISO-8601 format, illegal offset date time at index 5)") - ) - ) && - assert(stringify("2020-01-0").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal offset date time at index 8)") - ) - ) && - assert(stringify("2020-01-01T0").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T0 is not a valid ISO-8601 format, illegal offset date time at index 11)") - ) - ) && - assert(stringify("2020-01-01T01:0").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:0 is not a valid ISO-8601 format, illegal offset date time at index 14)") - ) - ) && - assert(stringify("X020-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(X020-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") - ) - ) && - assert(stringify("2X20-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2X20-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") - ) - ) && - assert(stringify("20X0-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(20X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") - ) - ) && - assert(stringify("202X-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(202X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") - ) - ) && - assert(stringify("2020X01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020X01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 4)") - ) - ) && - assert(stringify("2020-X1-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-X1-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") - ) - ) && - assert(stringify("2020-0X-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-0X-01T01:01Z is not a valid ISO-8601 format, expected digit at index 6)") - ) - ) && - assert(stringify("2020-01X01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01X01T01:01Z is not a valid ISO-8601 format, expected '-' at index 7)") - ) - ) && - assert(stringify("2020-01-X1T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-X1T01:01Z is not a valid ISO-8601 format, expected digit at index 8)") - ) - ) && - assert(stringify("2020-01-0XT01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-0XT01:01Z is not a valid ISO-8601 format, expected digit at index 9)") - ) - ) && - assert(stringify("2020-01-01X01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01X01:01Z is not a valid ISO-8601 format, expected 'T' at index 10)") - ) - ) && - assert(stringify("2020-01-01TX1:01").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01TX1:01 is not a valid ISO-8601 format, expected digit at index 11)") - ) - ) && - assert(stringify("2020-01-01T0X:01").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T0X:01 is not a valid ISO-8601 format, expected digit at index 12)") - ) - ) && - assert(stringify("2020-01-01T24:01").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T24:01 is not a valid ISO-8601 format, illegal hour at index 12)") - ) - ) && - assert(stringify("2020-01-01T01X01").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01X01 is not a valid ISO-8601 format, expected ':' at index 13)") - ) - ) && - assert(stringify("2020-01-01T01:X1").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:X1 is not a valid ISO-8601 format, expected digit at index 14)") - ) - ) && - assert(stringify("2020-01-01T01:0X").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:0X is not a valid ISO-8601 format, expected digit at index 15)") - ) - ) && - assert(stringify("2020-01-01T01:60").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:60 is not a valid ISO-8601 format, illegal minute at index 15)") - ) - ) && - assert(stringify("2020-01-01T01:01").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01 is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01X").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01X is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:0").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:0 is not a valid ISO-8601 format, illegal offset date time at index 17)") - ) - ) && - assert(stringify("2020-01-01T01:01:X1Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:X1Z is not a valid ISO-8601 format, expected digit at index 17)") - ) - ) && - assert(stringify("2020-01-01T01:01:0XZ").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:0XZ is not a valid ISO-8601 format, expected digit at index 18)") - ) - ) && - assert(stringify("2020-01-01T01:01:60Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:60Z is not a valid ISO-8601 format, illegal second at index 18)") - ) - ) && - assert(stringify("2020-01-01T01:01:01").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 19)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:012").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:012 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 19)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:01.").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01. is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 20)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:01.X").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01.X is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 20)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:01.123456789X").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01.123456789X is not a valid ISO-8601 format, expected '+' or '-' or 'Z' at index 29)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:01ZX").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01ZX is not a valid ISO-8601 format, illegal offset date time at index 20)") - ) - ) && - assert(stringify("2020-01-01T01:01:01+X1:01:01").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+X1:01:01 is not a valid ISO-8601 format, expected digit at index 20)") - ) - ) && - assert(stringify("2020-01-01T01:01:01+0").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+0 is not a valid ISO-8601 format, illegal offset date time at index 20)") - ) - ) && - assert(stringify("2020-01-01T01:01:01+0X:01:01").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+0X:01:01 is not a valid ISO-8601 format, expected digit at index 21)") - ) - ) && - assert(stringify("2020-01-01T01:01:01+19:01:01").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+19:01:01 is not a valid ISO-8601 format, illegal timezone offset hour at index 21)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:01+01X01:01").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01X01:01 is not a valid ISO-8601 format, illegal offset date time at index 23)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:01+01:0").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:0 is not a valid ISO-8601 format, illegal offset date time at index 23)") - ) - ) && - assert(stringify("2020-01-01T01:01:01+01:X1:01").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:X1:01 is not a valid ISO-8601 format, expected digit at index 23)") - ) - ) && - assert(stringify("2020-01-01T01:01:01+01:0X:01").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:0X:01 is not a valid ISO-8601 format, expected digit at index 24)") - ) - ) && - assert(stringify("2020-01-01T01:01:01+01:60:01").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:60:01 is not a valid ISO-8601 format, illegal timezone offset minute at index 24)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:01+01:01X01").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:01X01 is not a valid ISO-8601 format, illegal offset date time at index 26)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:01+01:01:0").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:01:0 is not a valid ISO-8601 format, illegal offset date time at index 26)" - ) - ) - ) && - assert(stringify("2020-01-01T01:01:01+01:01:X1").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:01:X1 is not a valid ISO-8601 format, expected digit at index 26)") - ) - ) && - assert(stringify("2020-01-01T01:01:01+01:01:0X").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:01:0X is not a valid ISO-8601 format, expected digit at index 27)") - ) - ) && - assert(stringify("2020-01-01T01:01:01+01:01:60").fromJson[OffsetDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:01:60 is not a valid ISO-8601 format, illegal timezone offset second at index 27)" - ) - ) - ) && - assert(stringify("+X0000-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+X0000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") - ) - ) && - assert(stringify("+1X000-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+1X000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") - ) - ) && - assert(stringify("+10X00-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+10X00-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") - ) - ) && - assert(stringify("+100X0-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+100X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 4)") - ) - ) && - assert(stringify("+1000X-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+1000X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") - ) - ) && - assert(stringify("+10000X-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+10000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 6)") - ) - ) && - assert(stringify("+100000X-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+100000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 7)") - ) - ) && - assert(stringify("+1000000X-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+1000000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 8)") - ) - ) && - assert(stringify("+1000000000-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+1000000000-01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 10)") - ) - ) && - assert(stringify("-1000000000-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(-1000000000-01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 10)") - ) - ) && - assert(stringify("-0000-01-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(-0000-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 4)") - ) - ) && - assert(stringify("+10000").fromJson[OffsetDateTime])( - isLeft( - equalTo("(+10000 is not a valid ISO-8601 format, illegal offset date time at index 6)") - ) - ) && - assert(stringify("2020-00-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-00-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") - ) - ) && - assert(stringify("2020-13-01T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-13-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") - ) - ) && - assert(stringify("2020-01-00T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-00T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-01-32T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-01-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-02-30T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-02-30T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-03-32T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-03-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-04-31T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-04-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-05-32T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-05-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-06-31T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-06-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-07-32T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-07-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-08-32T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-08-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-09-31T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-09-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-10-32T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-10-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-11-31T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-11-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) && - assert(stringify("2020-12-32T01:01Z").fromJson[OffsetDateTime])( - isLeft( - equalTo("(2020-12-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) - ) - }, - test("OffsetTime") { - assert(stringify("").fromJson[OffsetTime])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal offset time at index 0)") - ) - ) && - assert(stringify("0").fromJson[OffsetTime])( - isLeft( - equalTo("(0 is not a valid ISO-8601 format, illegal offset time at index 0)") - ) - ) && - assert(stringify("01:0").fromJson[OffsetTime])( - isLeft( - equalTo("(01:0 is not a valid ISO-8601 format, illegal offset time at index 3)") - ) - ) && - assert(stringify("X1:01").fromJson[OffsetTime])( - isLeft( - equalTo("(X1:01 is not a valid ISO-8601 format, expected digit at index 0)") - ) - ) && - assert(stringify("0X:01").fromJson[OffsetTime])( - isLeft( - equalTo("(0X:01 is not a valid ISO-8601 format, expected digit at index 1)") - ) - ) && - assert(stringify("24:01").fromJson[OffsetTime])( - isLeft( - equalTo("(24:01 is not a valid ISO-8601 format, illegal hour at index 1)") - ) - ) && - assert(stringify("01X01").fromJson[OffsetTime])( - isLeft( - equalTo("(01X01 is not a valid ISO-8601 format, expected ':' at index 2)") - ) - ) && - assert(stringify("01:X1").fromJson[OffsetTime])( - isLeft( - equalTo("(01:X1 is not a valid ISO-8601 format, expected digit at index 3)") - ) - ) && - assert(stringify("01:0X").fromJson[OffsetTime])( - isLeft( - equalTo("(01:0X is not a valid ISO-8601 format, expected digit at index 4)") - ) - ) && - assert(stringify("01:60").fromJson[OffsetTime])( - isLeft( - equalTo("(01:60 is not a valid ISO-8601 format, illegal minute at index 4)") - ) - ) && - assert(stringify("01:01").fromJson[OffsetTime])( - isLeft( - equalTo( - "(01:01 is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 5)" - ) - ) - ) && - assert(stringify("01:01X").fromJson[OffsetTime])( - isLeft( - equalTo( - "(01:01X is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 5)" - ) - ) - ) && - assert(stringify("01:01:0").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:0 is not a valid ISO-8601 format, illegal offset time at index 6)") - ) - ) && - assert(stringify("01:01:X1Z").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:X1Z is not a valid ISO-8601 format, expected digit at index 6)") - ) - ) && - assert(stringify("01:01:0XZ").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:0XZ is not a valid ISO-8601 format, expected digit at index 7)") - ) - ) && - assert(stringify("01:01:60Z").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:60Z is not a valid ISO-8601 format, illegal second at index 7)") - ) - ) && - assert(stringify("01:01:01").fromJson[OffsetTime])( - isLeft( - equalTo( - "(01:01:01 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 8)" - ) - ) - ) && - assert(stringify("01:01:012").fromJson[OffsetTime])( - isLeft( - equalTo( - "(01:01:012 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 8)" - ) - ) - ) && - assert(stringify("01:01:01.").fromJson[OffsetTime])( - isLeft( - equalTo( - "(01:01:01. is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 9)" - ) - ) - ) && - assert(stringify("01:01:01.X").fromJson[OffsetTime])( - isLeft( - equalTo( - "(01:01:01.X is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 9)" - ) - ) - ) && - assert(stringify("01:01:01.123456789X").fromJson[OffsetTime])( - isLeft( - equalTo( - "(01:01:01.123456789X is not a valid ISO-8601 format, expected '+' or '-' or 'Z' at index 18)" - ) - ) - ) && - assert(stringify("01:01:01ZX").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01ZX is not a valid ISO-8601 format, illegal offset time at index 9)") - ) - ) && - assert(stringify("01:01:01+X1:01:01").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+X1:01:01 is not a valid ISO-8601 format, expected digit at index 9)") - ) - ) && - assert(stringify("01:01:01+0").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+0 is not a valid ISO-8601 format, illegal offset time at index 9)") - ) - ) && - assert(stringify("01:01:01+0X:01:01").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+0X:01:01 is not a valid ISO-8601 format, expected digit at index 10)") - ) - ) && - assert(stringify("01:01:01+19:01:01").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+19:01:01 is not a valid ISO-8601 format, illegal timezone offset hour at index 10)") - ) - ) && - assert(stringify("01:01:01+01X01:01").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01X01:01 is not a valid ISO-8601 format, illegal offset time at index 12)") - ) - ) && - assert(stringify("01:01:01+01:0").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01:0 is not a valid ISO-8601 format, illegal offset time at index 12)") - ) - ) && - assert(stringify("01:01:01+01:X1:01").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01:X1:01 is not a valid ISO-8601 format, expected digit at index 12)") - ) - ) && - assert(stringify("01:01:01+01:0X:01").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01:0X:01 is not a valid ISO-8601 format, expected digit at index 13)") - ) - ) && - assert(stringify("01:01:01+01:60:01").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01:60:01 is not a valid ISO-8601 format, illegal timezone offset minute at index 13)") - ) - ) && - assert(stringify("01:01:01+01:01X01").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01:01X01 is not a valid ISO-8601 format, illegal offset time at index 15)") - ) - ) && - assert(stringify("01:01:01+01:01:0").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01:01:0 is not a valid ISO-8601 format, illegal offset time at index 15)") - ) - ) && - assert(stringify("01:01:01+01:01:X1").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01:01:X1 is not a valid ISO-8601 format, expected digit at index 15)") - ) - ) && - assert(stringify("01:01:01+01:01:0X").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01:01:0X is not a valid ISO-8601 format, expected digit at index 16)") - ) - ) && - assert(stringify("01:01:01+01:01:60").fromJson[OffsetTime])( - isLeft( - equalTo("(01:01:01+01:01:60 is not a valid ISO-8601 format, illegal timezone offset second at index 16)") - ) - ) - }, - test("Period") { - assert(stringify("").fromJson[Period])( - isLeft(equalTo("( is not a valid ISO-8601 format, illegal period at index 0)")) - ) && - assert(stringify("X").fromJson[Period])( - isLeft(equalTo("(X is not a valid ISO-8601 format, expected 'P' or '-' at index 0)")) - ) && - assert(stringify("P").fromJson[Period])( - isLeft(equalTo("(P is not a valid ISO-8601 format, illegal period at index 1)")) - ) && - assert(stringify("-").fromJson[Period])( - isLeft(equalTo("(- is not a valid ISO-8601 format, illegal period at index 1)")) - ) && - assert(stringify("PXY").fromJson[Period])( - isLeft(equalTo("(PXY is not a valid ISO-8601 format, expected '-' or digit at index 1)")) - ) && - assert(stringify("P-").fromJson[Period])( - isLeft(equalTo("(P- is not a valid ISO-8601 format, illegal period at index 2)")) - ) && - assert(stringify("P-XY").fromJson[Period])( - isLeft(equalTo("(P-XY is not a valid ISO-8601 format, expected digit at index 2)")) - ) && - assert(stringify("P1XY").fromJson[Period])( - isLeft( - equalTo("(P1XY is not a valid ISO-8601 format, expected 'Y' or 'M' or 'W' or 'D' or digit at index 2)") - ) - ) && - assert(stringify("P2147483648Y").fromJson[Period])( - isLeft(equalTo("(P2147483648Y is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P21474836470Y").fromJson[Period])( - isLeft(equalTo("(P21474836470Y is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P-2147483649Y").fromJson[Period])( - isLeft(equalTo("(P-2147483649Y is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P2147483648M").fromJson[Period])( - isLeft(equalTo("(P2147483648M is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P21474836470M").fromJson[Period])( - isLeft(equalTo("(P21474836470M is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P-2147483649M").fromJson[Period])( - isLeft(equalTo("(P-2147483649M is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P2147483648W").fromJson[Period])( - isLeft(equalTo("(P2147483648W is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P21474836470W").fromJson[Period])( - isLeft(equalTo("(P21474836470W is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P-2147483649W").fromJson[Period])( - isLeft(equalTo("(P-2147483649W is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P2147483648D").fromJson[Period])( - isLeft(equalTo("(P2147483648D is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P21474836470D").fromJson[Period])( - isLeft(equalTo("(P21474836470D is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P-2147483649D").fromJson[Period])( - isLeft(equalTo("(P-2147483649D is not a valid ISO-8601 format, illegal period at index 11)")) - ) && - assert(stringify("P1YXM").fromJson[Period])( - isLeft(equalTo("(P1YXM is not a valid ISO-8601 format, expected '-' or digit at index 3)")) - ) && - assert(stringify("P1Y-XM").fromJson[Period])( - isLeft(equalTo("(P1Y-XM is not a valid ISO-8601 format, expected digit at index 4)")) - ) && - assert(stringify("P1Y1XM").fromJson[Period])( - isLeft(equalTo("(P1Y1XM is not a valid ISO-8601 format, expected 'M' or 'W' or 'D' or digit at index 4)")) - ) && - assert(stringify("P1Y2147483648M").fromJson[Period])( - isLeft(equalTo("(P1Y2147483648M is not a valid ISO-8601 format, illegal period at index 13)")) - ) && - assert(stringify("P1Y21474836470M").fromJson[Period])( - isLeft(equalTo("(P1Y21474836470M is not a valid ISO-8601 format, illegal period at index 13)")) - ) && - assert(stringify("P1Y-2147483649M").fromJson[Period])( - isLeft(equalTo("(P1Y-2147483649M is not a valid ISO-8601 format, illegal period at index 13)")) - ) && - assert(stringify("P1Y2147483648W").fromJson[Period])( - isLeft(equalTo("(P1Y2147483648W is not a valid ISO-8601 format, illegal period at index 13)")) - ) && - assert(stringify("P1Y21474836470W").fromJson[Period])( - isLeft(equalTo("(P1Y21474836470W is not a valid ISO-8601 format, illegal period at index 13)")) - ) && - assert(stringify("P1Y-2147483649W").fromJson[Period])( - isLeft(equalTo("(P1Y-2147483649W is not a valid ISO-8601 format, illegal period at index 13)")) - ) && - assert(stringify("P1Y2147483648D").fromJson[Period])( - isLeft(equalTo("(P1Y2147483648D is not a valid ISO-8601 format, illegal period at index 13)")) - ) && - assert(stringify("P1Y21474836470D").fromJson[Period])( - isLeft(equalTo("(P1Y21474836470D is not a valid ISO-8601 format, illegal period at index 13)")) - ) && - assert(stringify("P1Y-2147483649D").fromJson[Period])( - isLeft(equalTo("(P1Y-2147483649D is not a valid ISO-8601 format, illegal period at index 13)")) - ) && - assert(stringify("P1Y1MXW").fromJson[Period])( - isLeft(equalTo("(P1Y1MXW is not a valid ISO-8601 format, expected '\"' or '-' or digit at index 5)")) - ) && - assert(stringify("P1Y1M-XW").fromJson[Period])( - isLeft(equalTo("(P1Y1M-XW is not a valid ISO-8601 format, expected digit at index 6)")) - ) && - assert(stringify("P1Y1M1XW").fromJson[Period])( - isLeft(equalTo("(P1Y1M1XW is not a valid ISO-8601 format, expected 'W' or 'D' or digit at index 6)")) - ) && - assert(stringify("P1Y1M306783379W").fromJson[Period])( - isLeft(equalTo("(P1Y1M306783379W is not a valid ISO-8601 format, illegal period at index 14)")) - ) && - assert(stringify("P1Y1M3067833790W").fromJson[Period])( - isLeft(equalTo("(P1Y1M3067833790W is not a valid ISO-8601 format, illegal period at index 14)")) - ) && - assert(stringify("P1Y1M-306783379W").fromJson[Period])( - isLeft(equalTo("(P1Y1M-306783379W is not a valid ISO-8601 format, illegal period at index 15)")) - ) && - assert(stringify("P1Y1M2147483648D").fromJson[Period])( - isLeft(equalTo("(P1Y1M2147483648D is not a valid ISO-8601 format, illegal period at index 15)")) - ) && - assert(stringify("P1Y1M21474836470D").fromJson[Period])( - isLeft(equalTo("(P1Y1M21474836470D is not a valid ISO-8601 format, illegal period at index 15)")) - ) && - assert(stringify("P1Y1M-2147483649D").fromJson[Period])( - isLeft(equalTo("(P1Y1M-2147483649D is not a valid ISO-8601 format, illegal period at index 15)")) - ) && - assert(stringify("P1Y1M1WXD").fromJson[Period])( - isLeft(equalTo("(P1Y1M1WXD is not a valid ISO-8601 format, expected '\"' or '-' or digit at index 7)")) - ) && - assert(stringify("P1Y1M1W-XD").fromJson[Period])( - isLeft(equalTo("(P1Y1M1W-XD is not a valid ISO-8601 format, expected digit at index 8)")) - ) && - assert(stringify("P1Y1M1W1XD").fromJson[Period])( - isLeft(equalTo("(P1Y1M1W1XD is not a valid ISO-8601 format, expected 'D' or digit at index 8)")) - ) && - assert(stringify("P1Y1M306783378W8D").fromJson[Period])( - isLeft(equalTo("(P1Y1M306783378W8D is not a valid ISO-8601 format, illegal period at index 16)")) - ) && - assert(stringify("P1Y1M-306783378W-8D").fromJson[Period])( - isLeft(equalTo("(P1Y1M-306783378W-8D is not a valid ISO-8601 format, illegal period at index 18)")) - ) && - assert(stringify("P1Y1M1W2147483647D").fromJson[Period])( - isLeft(equalTo("(P1Y1M1W2147483647D is not a valid ISO-8601 format, illegal period at index 17)")) + assert(stringify("2020-01-01T01:01:01ZX").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("P1Y1M-1W-2147483648D").fromJson[Period])( - isLeft(equalTo("(P1Y1M-1W-2147483648D is not a valid ISO-8601 format, illegal period at index 19)")) + assert(stringify("2020-01-01T01:01:01+X1:01:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("P1Y1M0W2147483648D").fromJson[Period])( - isLeft(equalTo("(P1Y1M0W2147483648D is not a valid ISO-8601 format, illegal period at index 17)")) + assert(stringify("2020-01-01T01:01:01+0").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("P1Y1M0W21474836470D").fromJson[Period])( - isLeft(equalTo("(P1Y1M0W21474836470D is not a valid ISO-8601 format, illegal period at index 17)")) + assert(stringify("2020-01-01T01:01:01+0X:01:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("P1Y1M0W-2147483649D").fromJson[Period])( - isLeft(equalTo("(P1Y1M0W-2147483649D is not a valid ISO-8601 format, illegal period at index 17)")) + assert(stringify("2020-01-01T01:01:01+19:01:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("P1Y1M1W1DX").fromJson[Period])( - isLeft(equalTo("(P1Y1M1W1DX is not a valid ISO-8601 format, illegal period at index 9)")) - ) - }, - test("Year") { - assert(stringify("").fromJson[Year])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal year at index 0)") - ) + assert(stringify("2020-01-01T01:01:01+01X01:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2").fromJson[Year])( - isLeft( - equalTo("(2 is not a valid ISO-8601 format, illegal year at index 0)") - ) + assert(stringify("2020-01-01T01:01:01+01:0").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("22").fromJson[Year])( - isLeft( - equalTo("(22 is not a valid ISO-8601 format, illegal year at index 0)") - ) + assert(stringify("2020-01-01T01:01:01+01:X1:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("222").fromJson[Year])( - isLeft( - equalTo("(222 is not a valid ISO-8601 format, illegal year at index 0)") - ) + assert(stringify("2020-01-01T01:01:01+01:0X:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("X020").fromJson[Year])( - isLeft( - equalTo("(X020 is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") - ) + assert(stringify("2020-01-01T01:01:01+01:60:01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2X20").fromJson[Year])( - isLeft( - equalTo("(2X20 is not a valid ISO-8601 format, expected digit at index 1)") - ) + assert(stringify("2020-01-01T01:01:01+01:01X01").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("20X0").fromJson[Year])( - isLeft( - equalTo("(20X0 is not a valid ISO-8601 format, expected digit at index 2)") - ) + assert(stringify("2020-01-01T01:01:01+01:01:0").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("202X").fromJson[Year])( - isLeft( - equalTo("(202X is not a valid ISO-8601 format, expected digit at index 3)") - ) + assert(stringify("2020-01-01T01:01:01+01:01:X1").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+X0000").fromJson[Year])( - isLeft( - equalTo("(+X0000 is not a valid ISO-8601 format, expected digit at index 1)") - ) + assert(stringify("2020-01-01T01:01:01+01:01:0X").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1X000").fromJson[Year])( - isLeft( - equalTo("(+1X000 is not a valid ISO-8601 format, expected digit at index 2)") - ) + assert(stringify("2020-01-01T01:01:01+01:01:60").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+10X00").fromJson[Year])( - isLeft( - equalTo("(+10X00 is not a valid ISO-8601 format, expected digit at index 3)") - ) + assert(stringify("+X0000-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+100X0").fromJson[Year])( - isLeft( - equalTo("(+100X0 is not a valid ISO-8601 format, expected digit at index 4)") - ) + assert(stringify("+1X000-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1000X").fromJson[Year])( - isLeft( - equalTo("(+1000X is not a valid ISO-8601 format, expected digit at index 5)") - ) + assert(stringify("+10X00-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+10000X").fromJson[Year])( - isLeft( - equalTo("(+10000X is not a valid ISO-8601 format, expected digit at index 6)") - ) + assert(stringify("+100X0-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+100000X").fromJson[Year])( - isLeft( - equalTo("(+100000X is not a valid ISO-8601 format, expected digit at index 7)") - ) + assert(stringify("+1000X-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1000000X").fromJson[Year])( - isLeft( - equalTo("(+1000000X is not a valid ISO-8601 format, expected digit at index 8)") - ) + assert(stringify("+10000X-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1000000000").fromJson[Year])( - isLeft( - equalTo("(+1000000000 is not a valid ISO-8601 format, illegal year at index 10)") - ) + assert(stringify("+100000X-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("-1000000000").fromJson[Year])( - isLeft( - equalTo("(-1000000000 is not a valid ISO-8601 format, illegal year at index 10)") - ) + assert(stringify("+1000000X-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("-0000").fromJson[Year])( - isLeft( - equalTo("(-0000 is not a valid ISO-8601 format, illegal year at index 4)") - ) + assert(stringify("+1000000000-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("10000").fromJson[Year])( - isLeft( - equalTo("(10000 is not a valid ISO-8601 format, illegal year at index 4)") - ) - ) - }, - test("YearMonth") { - assert(stringify("").fromJson[YearMonth])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal year month at index 0)") - ) + assert(stringify("-1000000000-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020").fromJson[YearMonth])( - isLeft( - equalTo("(2020 is not a valid ISO-8601 format, illegal year month at index 0)") - ) + assert(stringify("-0000-01-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-0").fromJson[YearMonth])( - isLeft( - equalTo("(2020-0 is not a valid ISO-8601 format, illegal year month at index 5)") - ) + assert(stringify("+10000").fromJson[OffsetDateTime])(isLeft(containsString("expected an OffsetDateTime"))) && + assert(stringify("2020-00-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-012").fromJson[YearMonth])( - isLeft( - equalTo("(2020-012 is not a valid ISO-8601 format, illegal year month at index 7)") - ) + assert(stringify("2020-13-01T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("X020-01").fromJson[YearMonth])( - isLeft( - equalTo("(X020-01 is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") - ) + assert(stringify("2020-01-00T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2X20-01").fromJson[YearMonth])( - isLeft( - equalTo("(2X20-01 is not a valid ISO-8601 format, expected digit at index 1)") - ) + assert(stringify("2020-01-32T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("20X0-01").fromJson[YearMonth])( - isLeft( - equalTo("(20X0-01 is not a valid ISO-8601 format, expected digit at index 2)") - ) + assert(stringify("2020-02-30T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("202X-01").fromJson[YearMonth])( - isLeft( - equalTo("(202X-01 is not a valid ISO-8601 format, expected digit at index 3)") - ) + assert(stringify("2020-03-32T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020X01").fromJson[YearMonth])( - isLeft( - equalTo("(2020X01 is not a valid ISO-8601 format, expected '-' at index 4)") - ) + assert(stringify("2020-04-31T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-X1").fromJson[YearMonth])( - isLeft( - equalTo("(2020-X1 is not a valid ISO-8601 format, expected digit at index 5)") - ) + assert(stringify("2020-05-32T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("2020-0X").fromJson[YearMonth])( - isLeft( - equalTo("(2020-0X is not a valid ISO-8601 format, expected digit at index 6)") - ) + assert(stringify("2020-06-31T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+X0000-01").fromJson[YearMonth])( - isLeft( - equalTo("(+X0000-01 is not a valid ISO-8601 format, expected digit at index 1)") - ) + assert(stringify("2020-07-32T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1X000-01").fromJson[YearMonth])( - isLeft( - equalTo("(+1X000-01 is not a valid ISO-8601 format, expected digit at index 2)") - ) + assert(stringify("2020-08-32T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+10X00-01").fromJson[YearMonth])( - isLeft( - equalTo("(+10X00-01 is not a valid ISO-8601 format, expected digit at index 3)") - ) + assert(stringify("2020-09-31T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+100X0-01").fromJson[YearMonth])( - isLeft( - equalTo("(+100X0-01 is not a valid ISO-8601 format, expected digit at index 4)") - ) + assert(stringify("2020-10-32T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+1000X-01").fromJson[YearMonth])( - isLeft( - equalTo("(+1000X-01 is not a valid ISO-8601 format, expected digit at index 5)") - ) + assert(stringify("2020-11-31T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) ) && - assert(stringify("+10000X-01").fromJson[YearMonth])( - isLeft( - equalTo("(+10000X-01 is not a valid ISO-8601 format, expected '-' or digit at index 6)") - ) + assert(stringify("2020-12-32T01:01Z").fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) + ) + }, + test("OffsetTime") { + assert(stringify("").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("0").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:0").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("X1:01").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("0X:01").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("24:01").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01X01").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:X1").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:0X").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:60").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01X").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:0").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:X1Z").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:0XZ").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:60Z").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:01").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:012").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:01.").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:01.X").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:01.123456789X").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("+100000X-01").fromJson[YearMonth])( - isLeft( - equalTo("(+100000X-01 is not a valid ISO-8601 format, expected '-' or digit at index 7)") - ) + assert(stringify("01:01:01ZX").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:01+X1:01:01").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("+1000000X-01").fromJson[YearMonth])( - isLeft( - equalTo("(+1000000X-01 is not a valid ISO-8601 format, expected '-' or digit at index 8)") - ) + assert(stringify("01:01:01+0").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:01+0X:01:01").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("+1000000000-01").fromJson[YearMonth])( - isLeft( - equalTo("(+1000000000-01 is not a valid ISO-8601 format, expected '-' at index 10)") - ) + assert(stringify("01:01:01+19:01:01").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("-1000000000-01").fromJson[YearMonth])( - isLeft( - equalTo("(-1000000000-01 is not a valid ISO-8601 format, expected '-' at index 10)") - ) + assert(stringify("01:01:01+01X01:01").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("-0000-01").fromJson[YearMonth])( - isLeft( - equalTo("(-0000-01 is not a valid ISO-8601 format, illegal year at index 4)") - ) + assert(stringify("01:01:01+01X").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:01+01:0").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:01+01:X1:01").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("+10000").fromJson[YearMonth])( - isLeft( - equalTo("(+10000 is not a valid ISO-8601 format, illegal year month at index 6)") - ) + assert(stringify("01:01:01+01:0X:01").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("2020-00").fromJson[YearMonth])( - isLeft( - equalTo("(2020-00 is not a valid ISO-8601 format, illegal month at index 6)") - ) + assert(stringify("01:01:01+01:60:01").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("2020-13").fromJson[YearMonth])( - isLeft( - equalTo("(2020-13 is not a valid ISO-8601 format, illegal month at index 6)") - ) - ) - }, - test("ZonedDateTime") { - assert(stringify("").fromJson[ZonedDateTime])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal zoned date time at index 0)") - ) + assert(stringify("01:01:01+01:01X").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && + assert(stringify("01:01:01+01:01X01").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("2020").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020 is not a valid ISO-8601 format, illegal zoned date time at index 0)") - ) + assert(stringify("01:01:01+01:01:0").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("2020-0").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-0 is not a valid ISO-8601 format, illegal zoned date time at index 5)") - ) + assert(stringify("01:01:01+01:01:X1").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && - assert(stringify("2020-01-0").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-0 is not a valid ISO-8601 format, illegal zoned date time at index 8)") - ) + assert(stringify("01:01:01+01:01:0X").fromJson[OffsetTime])( + isLeft(containsString("expected an OffsetTime")) ) && + assert(stringify("01:01:01+01:01:60").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) + }, + test("Period") { + assert(stringify("").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("X").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("-").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("PXY").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P-").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P-XY").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1XY").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P2147483648Y").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P21474836470Y").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P-2147483649Y").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P2147483648M").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P21474836470M").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P-2147483649M").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P2147483648W").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P21474836470W").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P-2147483649W").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P2147483648D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P21474836470D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P-2147483649D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1YXM").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y-XM").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1XM").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y2147483648M").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y21474836470M").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y-2147483649M").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y2147483648W").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y21474836470W").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y-2147483649W").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y2147483648D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y21474836470D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y-2147483649D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1MXW").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M-XW").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M1XW").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M306783379W").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M3067833790W").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M-306783379W").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M2147483648D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M21474836470D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M-2147483649D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M1WXD").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M1W-XD").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M1W1XD").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M306783378W8D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M-306783378W-8D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M1W2147483647D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M-1W-2147483648D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M0W2147483648D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M0W21474836470D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M0W-2147483649D").fromJson[Period])(isLeft(containsString("expected a Period"))) && + assert(stringify("P1Y1M1W1DX").fromJson[Period])(isLeft(containsString("expected a Period"))) + }, + test("Year") { + assert(stringify("").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("2").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("22").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("222").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("X020").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("2X20").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("20X0").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("202X").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("+X0000").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("+1X000").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("+10X00").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("+100X0").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("+1000X").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("+10000X").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("+100000X").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("+1000000X").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("+1000000000").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("-1000000000").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("-0000").fromJson[Year])(isLeft(containsString("expected a Year"))) && + assert(stringify("10000").fromJson[Year])(isLeft(containsString("expected a Year"))) + }, + test("YearMonth") { + assert(stringify("").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("2020").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("2020-0").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("2020-012").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("X020-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("2X20-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("20X0-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("202X-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("2020X01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("2020-X1").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("2020-0X").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+X0000-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+1X000-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+10X00-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+100X0-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+1000X-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+10000X-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+100000X-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+1000000X-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+1000000000-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("-1000000000-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("-0000-01").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("+10000").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("2020-00").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && + assert(stringify("2020-13").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) + }, + test("ZonedDateTime") { + assert(stringify("").fromJson[ZonedDateTime])(isLeft(containsString("expected a ZonedDateTime"))) && + assert(stringify("2020").fromJson[ZonedDateTime])(isLeft(containsString("expected a ZonedDateTime"))) && + assert(stringify("2020-0").fromJson[ZonedDateTime])(isLeft(containsString("expected a ZonedDateTime"))) && + assert(stringify("2020-01-0").fromJson[ZonedDateTime])(isLeft(containsString("expected a ZonedDateTime"))) && assert(stringify("2020-01-01T0").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T0 is not a valid ISO-8601 format, illegal zoned date time at index 11)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:0").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:0 is not a valid ISO-8601 format, illegal zoned date time at index 14)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("X020-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(X020-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or '+' or digit at index 0)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2X20-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2X20-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("20X0-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(20X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("202X-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(202X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020X01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020X01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 4)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-X1-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-X1-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-0X-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-0X-01T01:01Z is not a valid ISO-8601 format, expected digit at index 6)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01X01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01X01T01:01Z is not a valid ISO-8601 format, expected '-' at index 7)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-X1T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-X1T01:01Z is not a valid ISO-8601 format, expected digit at index 8)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-0XT01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-0XT01:01Z is not a valid ISO-8601 format, expected digit at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01X01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01X01:01Z is not a valid ISO-8601 format, expected 'T' at index 10)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01TX1:01").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01TX1:01 is not a valid ISO-8601 format, expected digit at index 11)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T0X:01").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T0X:01 is not a valid ISO-8601 format, expected digit at index 12)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T24:01").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T24:01 is not a valid ISO-8601 format, illegal hour at index 12)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01X01").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01X01 is not a valid ISO-8601 format, expected ':' at index 13)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:X1").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:X1 is not a valid ISO-8601 format, expected digit at index 14)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:0X").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:0X is not a valid ISO-8601 format, expected digit at index 15)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:60").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:60 is not a valid ISO-8601 format, illegal minute at index 15)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01 is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01X").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01X is not a valid ISO-8601 format, expected ':' or '+' or '-' or 'Z' at index 16)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:0").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:0 is not a valid ISO-8601 format, illegal zoned date time at index 17)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:X1Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:X1Z is not a valid ISO-8601 format, expected digit at index 17)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:0XZ").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:0XZ is not a valid ISO-8601 format, expected digit at index 18)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:60Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:60Z is not a valid ISO-8601 format, illegal second at index 18)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 19)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:012").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:012 is not a valid ISO-8601 format, expected '.' or '+' or '-' or 'Z' at index 19)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01.").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01. is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 20)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01.X").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01.X is not a valid ISO-8601 format, expected digit or '+' or '-' or 'Z' at index 20)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01.123456789X").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01.123456789X is not a valid ISO-8601 format, expected '+' or '-' or 'Z' at index 29)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01ZX").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01ZX is not a valid ISO-8601 format, expected '[' at index 20)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+X1:01:01").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+X1:01:01 is not a valid ISO-8601 format, expected digit at index 20)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+0").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+0 is not a valid ISO-8601 format, illegal zoned date time at index 20)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+0X:01:01").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+0X:01:01 is not a valid ISO-8601 format, expected digit at index 21)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+19:01:01").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+19:01:01 is not a valid ISO-8601 format, illegal timezone offset hour at index 21)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01X01:01").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01X01:01 is not a valid ISO-8601 format, expected '[' at index 22)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:0").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:0 is not a valid ISO-8601 format, illegal zoned date time at index 23)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:X1:01").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:X1:01 is not a valid ISO-8601 format, expected digit at index 23)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:0X:01").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:0X:01 is not a valid ISO-8601 format, expected digit at index 24)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:60:01").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:60:01 is not a valid ISO-8601 format, illegal timezone offset minute at index 24)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:01X01").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:01X01 is not a valid ISO-8601 format, expected '[' at index 25)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:01:0").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:01:0 is not a valid ISO-8601 format, illegal zoned date time at index 26)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:01:X1").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:01:X1 is not a valid ISO-8601 format, expected digit at index 26)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:01:0X").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:01:0X is not a valid ISO-8601 format, expected digit at index 27)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:01:01X").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01:01+01:01:01X is not a valid ISO-8601 format, expected '[' at index 28)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01:01+01:01:60").fromJson[ZonedDateTime])( - isLeft( - equalTo( - "(2020-01-01T01:01:01+01:01:60 is not a valid ISO-8601 format, illegal timezone offset second at index 27)" - ) - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("+X0000-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+X0000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 1)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("+1X000-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+1X000-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 2)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("+10X00-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+10X00-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 3)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("+100X0-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+100X0-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 4)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("+1000X-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+1000X-01-01T01:01Z is not a valid ISO-8601 format, expected digit at index 5)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("+10000X-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+10000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 6)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("+100000X-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+100000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 7)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("+1000000X-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+1000000X-01-01T01:01Z is not a valid ISO-8601 format, expected '-' or digit at index 8)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("+1000000000-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+1000000000-01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 10)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("-1000000000-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(-1000000000-01-01T01:01Z is not a valid ISO-8601 format, expected '-' at index 10)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("-0000-01-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(-0000-01-01T01:01Z is not a valid ISO-8601 format, illegal year at index 4)") - ) - ) && - assert(stringify("+10000").fromJson[ZonedDateTime])( - isLeft( - equalTo("(+10000 is not a valid ISO-8601 format, illegal zoned date time at index 6)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && + assert(stringify("+10000").fromJson[ZonedDateTime])(isLeft(containsString("expected a ZonedDateTime"))) && assert(stringify("2020-00-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-00-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-13-01T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-13-01T01:01Z is not a valid ISO-8601 format, illegal month at index 6)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-00T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-00T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-32T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-02-30T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-02-30T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-03-32T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-03-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-04-31T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-04-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-05-32T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-05-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-06-31T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-06-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-07-32T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-07-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-08-32T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-08-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-09-31T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-09-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-10-32T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-10-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-11-31T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-11-31T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-12-32T01:01Z").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-12-32T01:01Z is not a valid ISO-8601 format, illegal day at index 9)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01Z[").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01Z[ is not a valid ISO-8601 format, illegal zoned date time at index 17)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01Z[X]").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01Z[X] is not a valid ISO-8601 format, illegal zoned date time at index 18)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) && assert(stringify("2020-01-01T01:01Z[GMT]X").fromJson[ZonedDateTime])( - isLeft( - equalTo("(2020-01-01T01:01Z[GMT]X is not a valid ISO-8601 format, illegal zoned date time at index 22)") - ) + isLeft(containsString("expected a ZonedDateTime")) ) }, test("ZoneId") { - assert(stringify("America/New York").fromJson[ZoneId])( - isLeft(equalTo("(America/New York is not a valid ISO-8601 format, illegal zone id at index 0)")) - ) && - assert(stringify("Solar_System/Mars").fromJson[ZoneId])( - isLeft(equalTo("(Solar_System/Mars is not a valid ISO-8601 format, illegal zone id at index 0)")) - ) + assert(stringify("America/New York").fromJson[ZoneId])(isLeft(containsString("expected a ZoneId"))) && + assert(stringify("Solar_System/Mars").fromJson[ZoneId])(isLeft(containsString("expected a ZoneId"))) }, test("ZoneOffset") { - assert(stringify("").fromJson[ZoneOffset])( - isLeft( - equalTo("( is not a valid ISO-8601 format, illegal zone offset at index 0)") - ) - ) && - assert(stringify("X").fromJson[ZoneOffset])( - isLeft( - equalTo("(X is not a valid ISO-8601 format, expected '+' or '-' or 'Z' at index 0)") - ) - ) && - assert(stringify("+X1:01:01").fromJson[ZoneOffset])( - isLeft( - equalTo("(+X1:01:01 is not a valid ISO-8601 format, expected digit at index 1)") - ) - ) && - assert(stringify("+0").fromJson[ZoneOffset])( - isLeft( - equalTo("(+0 is not a valid ISO-8601 format, illegal zone offset at index 1)") - ) - ) && - assert(stringify("+0X:01:01").fromJson[ZoneOffset])( - isLeft( - equalTo("(+0X:01:01 is not a valid ISO-8601 format, expected digit at index 2)") - ) - ) && - assert(stringify("+19:01:01").fromJson[ZoneOffset])( - isLeft( - equalTo( - "(+19:01:01 is not a valid ISO-8601 format, illegal timezone offset hour at index 2)" - ) - ) - ) && - assert(stringify("+01X01:01").fromJson[ZoneOffset])( - isLeft( - equalTo( - "(+01X01:01 is not a valid ISO-8601 format, illegal zone offset at index 4)" - ) - ) - ) && - assert(stringify("+01:0").fromJson[ZoneOffset])( - isLeft( - equalTo("(+01:0 is not a valid ISO-8601 format, illegal zone offset at index 4)") - ) - ) && - assert(stringify("+01:X1:01").fromJson[ZoneOffset])( - isLeft( - equalTo("(+01:X1:01 is not a valid ISO-8601 format, expected digit at index 4)") - ) - ) && - assert(stringify("+01:0X:01").fromJson[ZoneOffset])( - isLeft( - equalTo("(+01:0X:01 is not a valid ISO-8601 format, expected digit at index 5)") - ) - ) && - assert(stringify("+01:60:01").fromJson[ZoneOffset])( - isLeft( - equalTo( - "(+01:60:01 is not a valid ISO-8601 format, illegal timezone offset minute at index 5)" - ) - ) - ) && - assert(stringify("+01:01X01").fromJson[ZoneOffset])( - isLeft( - equalTo( - "(+01:01X01 is not a valid ISO-8601 format, illegal zone offset at index 7)" - ) - ) - ) && - assert(stringify("+01:01:0").fromJson[ZoneOffset])( - isLeft( - equalTo( - "(+01:01:0 is not a valid ISO-8601 format, illegal zone offset at index 7)" - ) - ) - ) && - assert(stringify("+01:01:X1").fromJson[ZoneOffset])( - isLeft( - equalTo("(+01:01:X1 is not a valid ISO-8601 format, expected digit at index 7)") - ) - ) && - assert(stringify("+01:01:0X").fromJson[ZoneOffset])( - isLeft( - equalTo("(+01:01:0X is not a valid ISO-8601 format, expected digit at index 8)") - ) - ) && - assert(stringify("+01:01:60").fromJson[ZoneOffset])( - isLeft( - equalTo( - "(+01:01:60 is not a valid ISO-8601 format, illegal timezone offset second at index 8)" - ) - ) - ) + assert(stringify("").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("X").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+X1:01:01").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+0").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+0X:01:01").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+19:01:01").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01X01:01").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01X").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:0").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:X1:01").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:0X:01").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:60:01").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:01X").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:01X01").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:01:0").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:01:X1").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:01:0X").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && + assert(stringify("+01:01:60").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) } ) ) From 581c4dd8eb31efbb5787cb7c1186f8d150644310 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 4 Mar 2025 16:02:25 +0100 Subject: [PATCH 202/311] Yet more efficient decoders for `java.time._` values (#1355) --- .../src/main/scala/zio/json/JsonDecoder.scala | 78 +- .../main/scala/zio/json/internal/lexer.scala | 1555 ++++++++++++++++- .../scala/zio/json/javatime/parsers.scala | 14 +- 3 files changed, 1483 insertions(+), 164 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 5c072aa2c..b559c52c9 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -942,11 +942,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val duration: JsonDecoder[Duration] = new JsonDecoder[Duration] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Duration = - try parsers.unsafeParseDuration(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a Duration", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Duration = Lexer.duration(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Duration = { json match { @@ -961,11 +957,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val instant: JsonDecoder[Instant] = new JsonDecoder[Instant] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Instant = - try parsers.unsafeParseInstant(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected an Instant", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Instant = Lexer.instant(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Instant = { json match { @@ -980,11 +972,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val localDate: JsonDecoder[LocalDate] = new JsonDecoder[LocalDate] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): LocalDate = - try parsers.unsafeParseLocalDate(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a LocalDate", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): LocalDate = Lexer.localDate(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): LocalDate = { json match { @@ -999,11 +987,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val localDateTime: JsonDecoder[LocalDateTime] = new JsonDecoder[LocalDateTime] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): LocalDateTime = - try parsers.unsafeParseLocalDateTime(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a LocalDateTime", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): LocalDateTime = Lexer.localDateTime(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): LocalDateTime = { json match { @@ -1018,11 +1002,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val localTime: JsonDecoder[LocalTime] = new JsonDecoder[LocalTime] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): LocalTime = - try parsers.unsafeParseLocalTime(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a LocalTime", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): LocalTime = Lexer.localTime(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): LocalTime = { json match { @@ -1052,11 +1032,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val monthDay: JsonDecoder[MonthDay] = new JsonDecoder[MonthDay] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): MonthDay = - try parsers.unsafeParseMonthDay(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a MonthDay", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): MonthDay = Lexer.monthDay(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): MonthDay = { json match { @@ -1071,11 +1047,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val offsetDateTime: JsonDecoder[OffsetDateTime] = new JsonDecoder[OffsetDateTime] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): OffsetDateTime = - try parsers.unsafeParseOffsetDateTime(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected an OffsetDateTime", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): OffsetDateTime = Lexer.offsetDateTime(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): OffsetDateTime = { json match { @@ -1090,11 +1062,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val offsetTime: JsonDecoder[OffsetTime] = new JsonDecoder[OffsetTime] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): OffsetTime = - try parsers.unsafeParseOffsetTime(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected an OffsetTime", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): OffsetTime = Lexer.offsetTime(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): OffsetTime = { json match { @@ -1109,11 +1077,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val period: JsonDecoder[Period] = new JsonDecoder[Period] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Period = - try parsers.unsafeParsePeriod(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a Period", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Period = Lexer.period(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Period = { json match { @@ -1128,11 +1092,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val year: JsonDecoder[Year] = new JsonDecoder[Year] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): Year = - try parsers.unsafeParseYear(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a Year", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): Year = Lexer.year(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Year = { json match { @@ -1147,11 +1107,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val yearMonth: JsonDecoder[YearMonth] = new JsonDecoder[YearMonth] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): YearMonth = - try parsers.unsafeParseYearMonth(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a YearMonth", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): YearMonth = Lexer.yearMonth(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): YearMonth = { json match { @@ -1166,11 +1122,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val zonedDateTime: JsonDecoder[ZonedDateTime] = new JsonDecoder[ZonedDateTime] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): ZonedDateTime = - try parsers.unsafeParseZonedDateTime(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a ZonedDateTime", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): ZonedDateTime = Lexer.zonedDateTime(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): ZonedDateTime = { json match { @@ -1204,11 +1156,7 @@ private[json] trait DecoderLowPriority3 extends DecoderLowPriority4 { } } implicit val zoneOffset: JsonDecoder[ZoneOffset] = new JsonDecoder[ZoneOffset] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): ZoneOffset = - try parsers.unsafeParseZoneOffset(Lexer.string(trace, in).toString) - catch { - case _: DateTimeException => Lexer.error("expected a ZoneOffset", trace) - } + def unsafeDecode(trace: List[JsonError], in: RetractReader): ZoneOffset = Lexer.zoneOffset(trace, in) override def unsafeFromJsonAST(trace: List[JsonError], json: Json): ZoneOffset = { json match { diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 15e757e3b..5c7bec203 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -16,9 +16,9 @@ package zio.json.internal import zio.json.JsonDecoder.{ JsonError, UnsafeJson } - -import java.time.{ DayOfWeek, Month } +import java.time._ import java.util.UUID +import java.util.concurrent.ConcurrentHashMap import scala.annotation._ // tries to stick to the spec, but maybe a bit loose in places (e.g. numbers) @@ -148,6 +148,95 @@ object Lexer { else if (level != 0) skipArray(in, level - 1) } + def boolean(trace: List[JsonError], in: OneCharReader): Boolean = { + val c = in.nextNonWhitespace() + if (c == 't' && in.readChar() == 'r' && in.readChar() == 'u' && in.readChar() == 'e') true + else if (c == 'f' && in.readChar() == 'a' && in.readChar() == 'l' && in.readChar() == 's' && in.readChar() == 'e') + false + else error("expected a Boolean", c, trace) + } + + def byte(trace: List[JsonError], in: RetractReader): Byte = + try { + val i = UnsafeNumbers.byte_(in, false) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error("expected a Byte", trace) + } + + def short(trace: List[JsonError], in: RetractReader): Short = + try { + val i = UnsafeNumbers.short_(in, false) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error("expected a Short", trace) + } + + def int(trace: List[JsonError], in: RetractReader): Int = + try { + val i = UnsafeNumbers.int_(in, false) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error("expected an Int", trace) + } + + def long(trace: List[JsonError], in: RetractReader): Long = + try { + val i = UnsafeNumbers.long_(in, false) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error("expected a Long", trace) + } + + def bigInteger(trace: List[JsonError], in: RetractReader): java.math.BigInteger = + try { + val i = UnsafeNumbers.bigInteger_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits-bit BigInteger", trace) + } + + def bigInt(trace: List[JsonError], in: RetractReader): BigInt = + try { + val i = UnsafeNumbers.bigInt_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits-bit BigInt", trace) + } + + def float(trace: List[JsonError], in: RetractReader): Float = + try { + val i = UnsafeNumbers.float_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error("expected a Float", trace) + } + + def double(trace: List[JsonError], in: RetractReader): Double = + try { + val i = UnsafeNumbers.double_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error("expected a Double", trace) + } + + def bigDecimal(trace: List[JsonError], in: RetractReader): java.math.BigDecimal = + try { + val i = UnsafeNumbers.bigDecimal_(in, false, NumberMaxBits) + in.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => error(s"expected a BigDecimal with $NumberMaxBits-bit mantissa", trace) + } + // FIXME: remove in the next major version def streamingString(trace: List[JsonError], in: OneCharReader): java.io.Reader = { char(trace, in, '"') @@ -324,8 +413,1377 @@ object Lexer { uuidError(trace) } + def duration(trace: List[JsonError], in: OneCharReader): Duration = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos = 0 + var seconds = 0L + var nanos, state = 0 + if (pos >= i) durationError(trace) + var ch = cs(pos) + pos += 1 + val isNeg = ch == '-' + if (isNeg) { + if (pos >= i) durationError(trace) + ch = cs(pos) + pos += 1 + } + if (ch != 'P' || pos >= i) durationError(trace) + ch = cs(pos) + pos += 1 + while ({ + if (state == 0) { + if (ch == 'T') { + if (pos >= i) durationError(trace) + ch = cs(pos) + pos += 1 + state = 1 + } + } else if (state == 1) { + if (ch != 'T' || pos >= i) durationError(trace) + ch = cs(pos) + pos += 1 + } else if (state == 4 && pos >= i) durationError(trace) + val isNegX = ch == '-' + if (isNegX) { + if (pos >= i) durationError(trace) + ch = cs(pos) + pos += 1 + } + if (ch < '0' || ch > '9') durationError(trace) + var x: Long = ('0' - ch).toLong + while ( + (pos < i) && { + ch = cs(pos) + ch >= '0' && ch <= '9' + } + ) { + if ( + x < -922337203685477580L || { + x = x * 10 + ('0' - ch) + x > 0 + } + ) durationError(trace) + pos += 1 + } + if (!(isNeg ^ isNegX)) { + if (x == -9223372036854775808L) durationError(trace) + x = -x + } + if (ch == 'D' && state <= 0) { + if (x < -106751991167300L || x > 106751991167300L) durationError(trace) + seconds = x * 86400 + state = 1 + } else if (ch == 'H' && state <= 1) { + if (x < -2562047788015215L || x > 2562047788015215L) durationError(trace) + seconds = sumSeconds(x * 3600, seconds, trace) + state = 2 + } else if (ch == 'M' && state <= 2) { + if (x < -153722867280912930L || x > 153722867280912930L) durationError(trace) + seconds = sumSeconds(x * 60, seconds, trace) + state = 3 + } else if (ch == '.') { + pos += 1 + seconds = sumSeconds(x, seconds, trace) + var nanoDigitWeight = 100000000 + while ( + (pos < i) && { + ch = cs(pos) + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nanos += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + pos += 1 + } + if (ch != 'S') durationError(trace) + if (isNeg ^ isNegX) nanos = -nanos + state = 4 + } else if (ch == 'S') { + seconds = sumSeconds(x, seconds, trace) + state = 4 + } else durationError(trace) + pos += 1 + (pos < i) && { + ch = cs(pos) + pos += 1 + true + } + }) () + return Duration.ofSeconds(seconds, nanos.toLong) + } + durationError(trace) + } + + def instant(trace: List[JsonError], in: OneCharReader): Instant = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, year, month, day = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + val yearNeg = ch0 == '-' || (ch0 != '+' && instantError(trace)) + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ( + pos < i && { + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 10 + } + ) { + year = + if (year > 100000000) 2147483647 + else year * 10 + (ch - '0') + yearDigits += 1 + } + yearDigits == 10 && year > 1000000000 || yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } + } + } || pos + 5 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + val ch5 = cs(pos + 5) + pos += 6 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || ch5 != 'T' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForYearMonth(year, month)) + } + ) instantError(trace) + val epochDay = + epochDayForYear(year) + (dayOfYearForYearMonth(year, month) + day - 719529) // 719528 == days 0000 to 1970 + var epochSecond = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + val hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + epochSecond = hour * 3600 + (ch3 * 10 + ch4 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } + ) instantError(trace) + var nano = 0 + var ch = '0' + if (pos < i) { + ch = cs(pos) + pos += 1 + if (ch == ':') { + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + epochSecond += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) instantError(trace) + if (pos < i) { + ch = cs(pos) + pos += 1 + if (ch == '.') { + var nanoDigitWeight = 100000000 + while ( + pos < i && { + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + } + } + } + } + var offsetTotal = 0 + if (ch != 'Z') { + val offsetNeg = ch == '-' || (ch != '+' && instantError(trace)) + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' + } + ) instantError(trace) + if ( + pos < i && { + ch = cs(pos) + pos += 1 + ch == ':' + } + ) { + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) instantError(trace) + if ( + pos < i && { + ch = cs(pos) + pos += 1 + ch == ':' + } + ) { + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) instantError(trace) + } + } + if (offsetTotal > 64800) instantError(trace) // 64800 == 18 * 60 * 60 + if (offsetNeg) offsetTotal = -offsetTotal + } + if (pos == i) return Instant.ofEpochSecond(epochDay * 86400 + (epochSecond - offsetTotal), nano.toLong) + } + instantError(trace) + } + + def localDate(trace: List[JsonError], in: OneCharReader): LocalDate = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, year, month, day = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + val yearNeg = ch0 == '-' || (ch0 != '+' && localDateError(trace)) + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ( + pos < i && { + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + } + ) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } + } + } || pos + 5 != i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForYearMonth(year, month)) + } + ) localDateError(trace) + return LocalDate.of(year, month, day) + } + localDateError(trace) + } + + def localDateTime(trace: List[JsonError], in: OneCharReader): LocalDateTime = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, year, month, day = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + val yearNeg = ch0 == '-' || (ch0 != '+' && localDateTimeError(trace)) + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ( + pos < i && { + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + } + ) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } + } + } || pos + 5 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + val ch5 = cs(pos + 5) + pos += 6 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || ch5 != 'T' || day == 0 || month < 1 || month > 12 || + (day > 28 && day > maxDayForYearMonth(year, month)) + } + ) localDateTimeError(trace) + var hour, minute = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } + ) localDateTimeError(trace) + var second, nano = 0 + if (pos < i) { + if ( + cs(pos) != ':' || { + pos += 1 + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + } + ) localDateTimeError(trace) + if (pos < i) { + if ( + cs(pos) != '.' || { + pos += 1 + var nanoDigitWeight = 100000000 + var ch = '0' + while ( + pos < i && { + ch = cs(pos) + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + pos += 1 + } + pos != i + } + ) localDateTimeError(trace) + } + } + return LocalDateTime.of(year, month, day, hour, minute, second, nano) + } + localDateTimeError(trace) + } + + def localTime(trace: List[JsonError], in: OneCharReader): LocalTime = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, hour, minute = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } + ) localTimeError(trace) + var second, nano = 0 + if (pos < i) { + if ( + cs(pos) != ':' || { + pos += 1 + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + } + ) localTimeError(trace) + if (pos < i) { + if ( + cs(pos) != '.' || { + pos += 1 + var nanoDigitWeight = 100000000 + var ch = '0' + while ( + pos < i && { + ch = cs(pos) + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + } + ) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + pos += 1 + } + pos != i + } + ) localTimeError(trace) + } + } + return LocalTime.of(hour, minute, second, nano) + } + localTimeError(trace) + } + + def monthDay(trace: List[JsonError], in: OneCharReader): MonthDay = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var month, day = 0 + if ( + i != 7 || { + val ch0 = cs(0) + val ch1 = cs(1) + val ch2 = cs(2) + val ch3 = cs(3) + val ch4 = cs(4) + val ch5 = cs(5) + val ch6 = cs(6) + month = ch2 * 10 + ch3 - 528 // 528 == '0' * 11 + day = ch5 * 10 + ch6 - 528 // 528 == '0' * 11 + ch0 != '-' || ch1 != '-' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || ch4 != '-' || + ch5 < '0' || ch5 > '9' || ch6 < '0' || ch6 > '9' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForMonth(month)) + } + ) monthDayError(trace) + return MonthDay.of(month, day) + } + monthDayError(trace) + } + + def offsetDateTime(trace: List[JsonError], in: OneCharReader): OffsetDateTime = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, year, month, day = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + val yearNeg = ch0 == '-' || (ch0 != '+' && offsetDateTimeError(trace)) + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ( + pos < i && { + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + } + ) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } + } + } || pos + 5 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + val ch5 = cs(pos + 5) + pos += 6 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || ch5 != 'T' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForYearMonth(year, month)) + } + ) offsetDateTimeError(trace) + var hour, minute = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } || pos >= i + ) offsetDateTimeError(trace) + var second, nano = 0 + var ch = cs(pos) + pos += 1 + if (ch == ':') { + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } || pos >= i + ) offsetDateTimeError(trace) + ch = cs(pos) + pos += 1 + if (ch == '.') { + var nanoDigitWeight = 100000000 + while ({ + if (pos >= i) offsetDateTimeError(trace) + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + }) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + } + } + val zoneOffset = + if (ch == 'Z') ZoneOffset.UTC + else { + val offsetNeg = ch == '-' || (ch != '+' && offsetDateTimeError(trace)) + var offsetTotal = 0 + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' + } + ) offsetDateTimeError(trace) + if (pos < i) { + if ( + cs(pos) != ':' || { + pos += 1 + pos + 1 >= i + } || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) offsetDateTimeError(trace) + if (pos < i) { + if ( + cs(pos) != ':' || { + pos += 1 + pos + 1 >= i + } || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) offsetDateTimeError(trace) + } + } + if (offsetTotal > 64800) offsetDateTimeError(trace) + toZoneOffset(offsetNeg, offsetTotal) + } + if (pos == i) return OffsetDateTime.of(year, month, day, hour, minute, second, nano, zoneOffset) + } + offsetDateTimeError(trace) + } + + def offsetTime(trace: List[JsonError], in: OneCharReader): OffsetTime = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, hour, minute = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } || pos >= i + ) offsetTimeError(trace) + var second, nano = 0 + var ch = cs(pos) + pos += 1 + if (ch == ':') { + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } || pos >= i + ) offsetTimeError(trace) + ch = cs(pos) + pos += 1 + if (ch == '.') { + var nanoDigitWeight = 100000000 + while ({ + if (pos >= i) offsetTimeError(trace) + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + }) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + } + } + val zoneOffset = + if (ch == 'Z') ZoneOffset.UTC + else { + val offsetNeg = ch == '-' || (ch != '+' && offsetTimeError(trace)) + var offsetTotal = 0 + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' + } + ) offsetTimeError(trace) + if (pos < i) { + if ( + cs(pos) != ':' || { + pos += 1 + pos + 1 >= i + } || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) offsetTimeError(trace) + if (pos < i) { + if ( + cs(pos) != ':' || { + pos += 1 + pos + 1 >= i + } || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) offsetTimeError(trace) + } + } + if (offsetTotal > 64800) offsetTimeError(trace) + toZoneOffset(offsetNeg, offsetTotal) + } + if (pos == i) return OffsetTime.of(hour, minute, second, nano, zoneOffset) + } + offsetTimeError(trace) + } + + def period(trace: List[JsonError], in: OneCharReader): Period = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, state, years, months, days = 0 + if (pos >= i) periodError(trace) + var ch = cs(pos) + pos += 1 + val isNeg = ch == '-' + if (isNeg) { + if (pos >= i) periodError(trace) + ch = cs(pos) + pos += 1 + } + if (ch != 'P' || pos >= i) periodError(trace) + ch = cs(pos) + pos += 1 + while ({ + if (state == 4 && pos >= i) periodError(trace) + val isNegX = ch == '-' + if (isNegX) { + if (pos >= i) periodError(trace) + ch = cs(pos) + pos += 1 + } + if (ch < '0' || ch > '9') periodError(trace) + var x: Int = '0' - ch + while ( + (pos < i) && { + ch = cs(pos) + ch >= '0' && ch <= '9' + } + ) { + if ( + x < -214748364 || { + x = x * 10 + ('0' - ch) + x > 0 + } + ) periodError(trace) + pos += 1 + } + if (!(isNeg ^ isNegX)) { + if (x == -2147483648) periodError(trace) + x = -x + } + if (ch == 'Y' && state <= 0) { + years = x + state = 1 + } else if (ch == 'M' && state <= 1) { + months = x + state = 2 + } else if (ch == 'W' && state <= 2) { + if (x < -306783378 || x > 306783378) periodError(trace) + days = x * 7 + state = 3 + } else if (ch == 'D') { + val ds = x.toLong + days + if (ds != ds.toInt) periodError(trace) + days = ds.toInt + state = 4 + } else periodError(trace) + pos += 1 + (pos < i) && { + ch = cs(pos) + pos += 1 + true + } + }) () + return Period.of(years, months, days) + } + periodError(trace) + } + + def year(trace: List[JsonError], in: OneCharReader): Year = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, year = 0 + if ( + pos + 3 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + pos += 4 + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + pos != i + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && yearError(trace)) + year = ch1 * 100 + ch2 * 10 + ch3 - 5328 // 53328 == '0' * 111 + var yearDigits = 3 + var ch = '0' + while ( + pos < i && { + ch = cs(pos) + ch >= '0' && ch <= '9' && yearDigits < 9 + } + ) { + year = year * 10 + (ch - '0') + yearDigits += 1 + pos += 1 + } + yearNeg && { + year = -year + year == 0 + } || pos != i + } + } + } + ) yearError(trace) + return Year.of(year) + } + yearError(trace) + } + + def yearMonth(trace: List[JsonError], in: OneCharReader): YearMonth = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, year, month = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && yearMonthError(trace)) + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ({ + if (pos >= i) yearMonthError(trace) + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + }) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } + } + } || pos + 2 != i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || month < 1 || month > 12 + } + ) yearMonthError(trace) + return YearMonth.of(year, month) + } + yearMonthError(trace) + } + + def zonedDateTime(trace: List[JsonError], in: OneCharReader): ZonedDateTime = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos, year, month, day, hour, minute = 0 + if ( + pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + ch1 < '0' || ch1 > '9' || ch2 < '0' || ch2 > '9' || ch3 < '0' || ch3 > '9' || { + if (ch0 >= '0' && ch0 <= '9') { + year = ch0 * 1000 + ch1 * 100 + ch2 * 10 + ch3 - 53328 // 53328 == '0' * 1111 + ch4 != '-' + } else { + val yearNeg = ch0 == '-' || (ch0 != '+' && zonedDateTimeError(trace)) + year = ch1 * 1000 + ch2 * 100 + ch3 * 10 + ch4 - 53328 // 53328 == '0' * 1111 + ch4 < '0' || ch4 > '9' || { + var yearDigits = 4 + var ch = '0' + while ({ + if (pos >= i) zonedDateTimeError(trace) + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && yearDigits < 9 + }) { + year = year * 10 + (ch - '0') + yearDigits += 1 + } + yearNeg && { + year = -year + year == 0 + } || ch != '-' + } + } + } + } || pos + 5 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + val ch5 = cs(pos + 5) + pos += 6 + month = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + day = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != '-' || ch3 < '0' || ch3 > '9' || + ch4 < '0' || ch4 > '9' || ch5 != 'T' || month < 1 || month > 12 || day == 0 || + (day > 28 && day > maxDayForYearMonth(year, month)) + } || pos + 4 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + val ch2 = cs(pos + 2) + val ch3 = cs(pos + 3) + val ch4 = cs(pos + 4) + pos += 5 + hour = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + minute = ch3 * 10 + ch4 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch2 != ':' || + ch3 < '0' || ch3 > '9' || ch4 < '0' || ch4 > '9' || ch3 > '5' || hour > 23 + } || pos >= i + ) zonedDateTimeError(trace) + var second, nano = 0 + var ch = cs(pos) + pos += 1 + if (ch == ':') { + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + second = ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } || pos >= i + ) zonedDateTimeError(trace) + ch = cs(pos) + pos += 1 + if (ch == '.') { + var nanoDigitWeight = 100000000 + while ({ + if (pos >= i) zonedDateTimeError(trace) + ch = cs(pos) + pos += 1 + ch >= '0' && ch <= '9' && nanoDigitWeight != 0 + }) { + nano += (ch - '0') * nanoDigitWeight + nanoDigitWeight = (nanoDigitWeight * 3435973837L >> 35).toInt // divide a positive int by 10 + } + } + } + val localDateTime = LocalDateTime.of(year, month, day, hour, minute, second, nano) + val zoneOffset = + if (ch == 'Z') { + if (pos < i) { + ch = cs(pos) + if (ch != '[') zonedDateTimeError(trace) + pos += 1 + } + ZoneOffset.UTC + } else { + val offsetNeg = ch == '-' || (ch != '+' && zonedDateTimeError(trace)) + var offsetTotal = 0 + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' + } + ) zonedDateTimeError(trace) + if ( + pos < i && { + ch = cs(pos) + pos += 1 + ch == ':' || ch != '[' && zonedDateTimeError(trace) + } + ) { + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) zonedDateTimeError(trace) + if ( + pos < i && { + ch = cs(pos) + pos += 1 + ch == ':' || ch != '[' && zonedDateTimeError(trace) + } + ) { + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) zonedDateTimeError(trace) + if (pos < i) { + ch = cs(pos) + if (ch != '[') zonedDateTimeError(trace) + pos += 1 + } + } + } + if (offsetTotal > 64800) zonedDateTimeError(trace) + toZoneOffset(offsetNeg, offsetTotal) + } + if (ch == '[') { + var zoneId: ZoneId = null + val from = pos + while ({ + if (pos >= i) zonedDateTimeError(trace) + ch = cs(pos) + ch != ']' + }) pos += 1 + val key = new String(cs, from, pos - from) + zoneId = zoneIds.get(key) + if ( + (zoneId eq null) && { + try zoneId = ZoneId.of(key) + catch { + case _: DateTimeException => zonedDateTimeError(trace) + } + !zoneId.isInstanceOf[ZoneOffset] || zoneId.asInstanceOf[ZoneOffset].getTotalSeconds % 900 == 0 + } + ) zoneIds.put(key, zoneId) + if (pos + 1 == i) return ZonedDateTime.ofInstant(localDateTime, zoneOffset, zoneId) + } else { + if (pos == i) return ZonedDateTime.ofLocal(localDateTime, zoneOffset, null) + } + } + zonedDateTimeError(trace) + } + + def zoneOffset(trace: List[JsonError], in: OneCharReader): ZoneOffset = { + var c = in.nextNonWhitespace() + if (c == '"') { + val cs = charArrays.get + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + var pos = 0 + if (pos >= i) zoneOffsetError(trace) + val ch = cs(pos) + pos += 1 + if (ch == 'Z') { + if (pos == i) return ZoneOffset.UTC + } else { + val offsetNeg = ch == '-' || (ch != '+' && zoneOffsetError(trace)) + var offsetTotal = 0 + if ( + pos + 1 >= i || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + offsetTotal = (ch0 * 10 + ch1 - 528) * 3600 // 528 == '0' * 11 + pos += 2 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' + } + ) zoneOffsetError(trace) + if (pos < i) { + if ( + cs(pos) != ':' || { + pos += 1 + pos + 1 >= i + } || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += (ch0 * 10 + ch1 - 528) * 60 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) zoneOffsetError(trace) + if (pos < i) { + if ( + cs(pos) != ':' || { + pos += 1 + pos + 1 >= i + } || { + val ch0 = cs(pos) + val ch1 = cs(pos + 1) + pos += 2 + offsetTotal += ch0 * 10 + ch1 - 528 // 528 == '0' * 11 + ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' + } + ) zoneOffsetError(trace) + } + } + if (offsetTotal <= 64800 && pos == i) return toZoneOffset(offsetNeg, offsetTotal) + } + } + zoneOffsetError(trace) + } + + private[this] def toZoneOffset(offsetNeg: Boolean, offsetTotal: Int): ZoneOffset = { + var qp = offsetTotal * 37283 + if ((qp & 0x1ff8000) == 0) { // check if offsetTotal divisible by 900 + qp >>>= 25 // divide offsetTotal by 900 + if (offsetNeg) qp = -qp + var zoneOffset = zoneOffsets(qp + 72) + if (zoneOffset ne null) zoneOffset + else { + zoneOffset = ZoneOffset.ofTotalSeconds(if (offsetNeg) -offsetTotal else offsetTotal) + zoneOffsets(qp + 72) = zoneOffset + zoneOffset + } + } else ZoneOffset.ofTotalSeconds(if (offsetNeg) -offsetTotal else offsetTotal) + } + + private[this] def sumSeconds(s1: Long, s2: Long, trace: List[JsonError]): Long = { + val s = s1 + s2 + if (((s1 ^ s) & (s2 ^ s)) < 0) durationError(trace) + s + } + + private[this] def epochDayForYear(year: Int): Long = + year * 365L + ((year + 3 >> 2) - { + val cp = year * 1374389535L + if (year < 0) (cp >> 37) - (cp >> 39) // year / 100 - year / 400 + else (cp + 136064563965L >> 37) - (cp + 548381424465L >> 39) // (year + 99) / 100 - (year + 399) / 400 + }.toInt) + + private[this] def dayOfYearForYearMonth(year: Int, month: Int): Int = + (month * 1002277 - 988622 >> 15) - // (month * 367 - 362) / 12 + (if (month <= 2) 0 + else if (isLeap(year)) 1 + else 2) + + private[this] def maxDayForMonth(month: Int): Int = + if (month != 2) ((month >> 3) ^ (month & 0x1)) + 30 + else 29 + + private[this] def maxDayForYearMonth(year: Int, month: Int): Int = + if (month != 2) ((month >> 3) ^ (month & 0x1)) + 30 + else if (isLeap(year)) 29 + else 28 + + private[this] def isLeap(year: Int): Boolean = (year & 0x3) == 0 && { // (year % 100 != 0 || year % 400 == 0) + val cp = year * 1374389535L + val cc = year >> 31 + ((cp ^ cc) & 0x1fc0000000L) != 0 || (((cp >> 37).toInt - cc) & 0x3) == 0 + } + @noinline private[this] def uuidError(trace: List[JsonError]): Nothing = error("expected a UUID", trace) + @noinline private[this] def durationError(trace: List[JsonError]): Nothing = error("expected a Duration", trace) + + @noinline private[this] def instantError(trace: List[JsonError]): Nothing = error("expected an Instant", trace) + + @noinline private[this] def localDateError(trace: List[JsonError]): Nothing = error("expected a LocalDate", trace) + + @noinline private[this] def localDateTimeError(trace: List[JsonError]): Nothing = + error("expected a LocalDateTime", trace) + + @noinline private[this] def localTimeError(trace: List[JsonError]): Nothing = error("expected a LocalTime", trace) + + @noinline private[this] def monthDayError(trace: List[JsonError]): Nothing = error("expected a MonthDay", trace) + + @noinline private[this] def offsetDateTimeError(trace: List[JsonError]): Nothing = + error("expected an OffsetDateTime", trace) + + @noinline private[this] def offsetTimeError(trace: List[JsonError]): Nothing = error("expected an OffsetTime", trace) + + @noinline private[this] def periodError(trace: List[JsonError]): Nothing = error("expected a Period", trace) + + @noinline private[this] def yearError(trace: List[JsonError]): Nothing = error("expected a Year", trace) + + @noinline private[this] def yearMonthError(trace: List[JsonError]): Nothing = error("expected a YearMonth", trace) + + @noinline private[this] def zonedDateTimeError(trace: List[JsonError]): Nothing = + error("expected a ZonedDateTime", trace) + + @noinline private[this] def zoneIdError(trace: List[JsonError]): Nothing = error("expected a ZoneId", trace) + + @noinline private[this] def zoneOffsetError(trace: List[JsonError]): Nothing = error("expected a ZoneOffset", trace) + private[this] val charArrays = new ThreadLocal[Array[Char]] { override def initialValue(): Array[Char] = new Array[Char](1024) // should be longer than 256 } @@ -358,6 +1816,10 @@ object Lexer { ns } + private[this] final val zoneOffsets: Array[ZoneOffset] = new Array(145) + + private[this] final val zoneIds: ConcurrentHashMap[String, ZoneId] = new ConcurrentHashMap(256) + def char(trace: List[JsonError], in: OneCharReader): Char = { var c = in.nextNonWhitespace() if (c != '"') error("'\"'", c, trace) @@ -399,95 +1861,6 @@ object Lexer { accum.toChar } - def boolean(trace: List[JsonError], in: OneCharReader): Boolean = { - val c = in.nextNonWhitespace() - if (c == 't' && in.readChar() == 'r' && in.readChar() == 'u' && in.readChar() == 'e') true - else if (c == 'f' && in.readChar() == 'a' && in.readChar() == 'l' && in.readChar() == 's' && in.readChar() == 'e') - false - else error("expected a Boolean", c, trace) - } - - def byte(trace: List[JsonError], in: RetractReader): Byte = - try { - val i = UnsafeNumbers.byte_(in, false) - in.retract() - i - } catch { - case UnsafeNumbers.UnsafeNumber => error("expected a Byte", trace) - } - - def short(trace: List[JsonError], in: RetractReader): Short = - try { - val i = UnsafeNumbers.short_(in, false) - in.retract() - i - } catch { - case UnsafeNumbers.UnsafeNumber => error("expected a Short", trace) - } - - def int(trace: List[JsonError], in: RetractReader): Int = - try { - val i = UnsafeNumbers.int_(in, false) - in.retract() - i - } catch { - case UnsafeNumbers.UnsafeNumber => error("expected an Int", trace) - } - - def long(trace: List[JsonError], in: RetractReader): Long = - try { - val i = UnsafeNumbers.long_(in, false) - in.retract() - i - } catch { - case UnsafeNumbers.UnsafeNumber => error("expected a Long", trace) - } - - def bigInteger(trace: List[JsonError], in: RetractReader): java.math.BigInteger = - try { - val i = UnsafeNumbers.bigInteger_(in, false, NumberMaxBits) - in.retract() - i - } catch { - case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits-bit BigInteger", trace) - } - - def bigInt(trace: List[JsonError], in: RetractReader): BigInt = - try { - val i = UnsafeNumbers.bigInt_(in, false, NumberMaxBits) - in.retract() - i - } catch { - case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits-bit BigInt", trace) - } - - def float(trace: List[JsonError], in: RetractReader): Float = - try { - val i = UnsafeNumbers.float_(in, false, NumberMaxBits) - in.retract() - i - } catch { - case UnsafeNumbers.UnsafeNumber => error("expected a Float", trace) - } - - def double(trace: List[JsonError], in: RetractReader): Double = - try { - val i = UnsafeNumbers.double_(in, false, NumberMaxBits) - in.retract() - i - } catch { - case UnsafeNumbers.UnsafeNumber => error("expected a Double", trace) - } - - def bigDecimal(trace: List[JsonError], in: RetractReader): java.math.BigDecimal = - try { - val i = UnsafeNumbers.bigDecimal_(in, false, NumberMaxBits) - in.retract() - i - } catch { - case UnsafeNumbers.UnsafeNumber => error(s"expected a BigDecimal with $NumberMaxBits-bit mantissa", trace) - } - def dayOfWeek(trace: List[JsonError], in: OneCharReader): DayOfWeek = { var c = in.nextNonWhitespace() if (c == '"') { diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala index 6e63c1171..0af67595f 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala @@ -417,7 +417,7 @@ private[json] object parsers { ch0 < '0' || ch0 > '9' || ch1 < '0' || ch1 > '9' || ch0 > '5' } } - ) localTimeError() + ) localDateTimeError() if (pos < len) { if ( input.charAt(pos) != '.' || { @@ -664,9 +664,8 @@ private[json] object parsers { } def unsafeParseOffsetTime(input: String): OffsetTime = { - val len = input.length - var pos = 0 - var hour, minute = 0 + val len = input.length + var pos, hour, minute = 0 if ( pos + 4 >= len || { val ch0 = input.charAt(pos) @@ -1111,8 +1110,8 @@ private[json] object parsers { } def unsafeParseZoneOffset(input: String): ZoneOffset = { - val len = input.length - var pos, nanoDigitWeight = 0 + val len = input.length + var pos = 0 if (pos >= len) zoneOffsetError() val ch = input.charAt(pos) pos += 1 @@ -1120,8 +1119,7 @@ private[json] object parsers { if (pos != len) zoneOffsetError() ZoneOffset.UTC } else { - val offsetNeg = ch == '-' || (ch != '+' && zoneOffsetError()) - nanoDigitWeight = -3 + val offsetNeg = ch == '-' || (ch != '+' && zoneOffsetError()) var offsetTotal = 0 if ( pos + 1 >= len || { From 44d5fb0edb7bf11e5097033f10a14c7c1740808d Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 4 Mar 2025 16:40:27 +0100 Subject: [PATCH 203/311] Update Ubuntu on CI (#1356) --- .github/workflows/auto-approve.yml | 2 +- .github/workflows/ci.yml | 68 ++++++++++++--------------- .github/workflows/release-drafter.yml | 2 +- .github/workflows/site.yml | 33 ++++++------- 4 files changed, 48 insertions(+), 57 deletions(-) diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index d183bced4..df70adf48 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -5,7 +5,7 @@ on: jobs: auto-approve: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: hmarr/auto-approve-action@v3.2.0 if: github.actor == 'scala-steward' || github.actor == 'renovate[bot]' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 787b927c0..4e187b434 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,26 +14,25 @@ on: jobs: lint: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 30 steps: - name: Checkout current branch uses: actions/checkout@v4.1.2 with: fetch-depth: 0 - - name: Setup Java - uses: actions/setup-java@v4.2.1 + - name: Setup Action + uses: coursier/setup-action@v1 with: - distribution: temurin - java-version: 11 - check-latest: true + jvm: temurin:11 + apps: sbt - name: Cache scala dependencies uses: coursier/cache-action@v6 - name: Lint code run: sbt "++2.12; check; ++2.13; check; ++3.3; check" benchmarks: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: @@ -44,36 +43,34 @@ jobs: uses: actions/checkout@v4.1.2 with: fetch-depth: 0 - - name: Setup Java - uses: actions/setup-java@v4.2.1 + - name: Setup Action + uses: coursier/setup-action@v1 with: - distribution: temurin - java-version: ${{ matrix.java }} - check-latest: true + jvm: temurin:${{ matrix.java }} + apps: sbt - name: Cache scala dependencies uses: coursier/cache-action@v6 - name: Compile benchmarks run: sbt ++${{ matrix.scala }}! jmh:compile mdoc: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 60 steps: - name: Checkout current branch uses: actions/checkout@v4.1.2 - - name: Setup Java - uses: actions/setup-java@v4.2.1 + - name: Setup Action + uses: coursier/setup-action@v1 with: - distribution: temurin - java-version: 11 - check-latest: true + jvm: temurin:11 + apps: sbt - name: Cache scala dependencies uses: coursier/cache-action@v6 - name: Check Document Generation run: sbt compileDocs test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 30 strategy: fail-fast: false @@ -89,12 +86,11 @@ jobs: - name: Install Boehm GC if: ${{ startsWith(matrix.platform, 'Native') }} run: sudo apt-get update && sudo apt-get install -y libgc-dev - - name: Setup Java - uses: actions/setup-java@v4.2.1 + - name: Setup Action + uses: coursier/setup-action@v1 with: - distribution: temurin - java-version: ${{ matrix.java }} - check-latest: true + jvm: temurin:${{ matrix.java }} + apps: sbt - name: Cache scala dependencies uses: coursier/cache-action@v6 - name: Install libuv @@ -107,7 +103,7 @@ jobs: run: sbt ++${{ matrix.scala }}! test${{ matrix.platform }} mima_check: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 30 steps: - name: Checkout current branch @@ -116,23 +112,22 @@ jobs: fetch-depth: 300 - name: Fetch tags run: git fetch --depth=300 origin +refs/tags/*:refs/tags/* - - name: Setup Java (temurin@21) - uses: actions/setup-java@v4 + - name: Setup Action + uses: coursier/setup-action@v1 with: - distribution: temurin - java-version: 11 - cache: sbt + jvm: temurin:11 + apps: sbt - run: sbt +mimaReportBinaryIssues ci: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: [lint, mdoc, benchmarks, test, mima_check] steps: - name: Aggregate of lint, and all tests run: echo "ci passed" publish: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 timeout-minutes: 60 needs: [ci] if: github.event_name != 'pull_request' @@ -141,12 +136,11 @@ jobs: uses: actions/checkout@v4.1.2 with: fetch-depth: 0 - - name: Setup Java - uses: actions/setup-java@v4.2.1 + - name: Setup Action + uses: coursier/setup-action@v1 with: - distribution: temurin - java-version: 11 - check-latest: true + jvm: temurin:11 + apps: sbt - name: Release run: sbt ci-release env: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index f0b96769c..74d030b0a 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -6,7 +6,7 @@ on: jobs: update_release_draft: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: release-drafter/release-drafter@v5 env: diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 36f866b3f..26cd3895d 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -14,38 +14,36 @@ name: Website jobs: build: name: Build and Test - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 if: ${{ github.event_name == 'pull_request' }} steps: - name: Git Checkout uses: actions/checkout@v3.3.0 with: fetch-depth: '0' - - name: Setup Scala - uses: actions/setup-java@v3.9.0 + - name: Setup Action + uses: coursier/setup-action@v1 with: - distribution: temurin - java-version: 17 - check-latest: true + jvm: temurin:17 + apps: sbt - name: Check artifacts build process run: sbt +publishLocal - name: Check website build process run: sbt docs/clean; sbt docs/buildWebsite publish-docs: name: Publish Docs - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 if: ${{ ((github.event_name == 'release') && (github.event.action == 'published')) || (github.event_name == 'workflow_dispatch') }} steps: - name: Git Checkout uses: actions/checkout@v3.3.0 with: fetch-depth: '0' - - name: Setup Scala - uses: actions/setup-java@v3.9.0 + - name: Setup Action + uses: coursier/setup-action@v1 with: - distribution: temurin - java-version: 17 - check-latest: true + jvm: temurin:17 + apps: sbt - name: Setup NodeJs uses: actions/setup-node@v3 with: @@ -57,7 +55,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} generate-readme: name: Generate README - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 if: ${{ (github.event_name == 'push') || ((github.event_name == 'release') && (github.event.action == 'published')) }} steps: - name: Git Checkout @@ -65,12 +63,11 @@ jobs: with: ref: ${{ github.head_ref }} fetch-depth: '0' - - name: Setup Scala - uses: actions/setup-java@v3.9.0 + - name: Setup Action + uses: coursier/setup-action@v1 with: - distribution: temurin - java-version: 17 - check-latest: true + jvm: temurin:17 + apps: sbt - name: Commit Changes run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" From 3bb283d1a42ad132282ced3a2139b925ec6ab0f5 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 6 Mar 2025 06:38:54 +0100 Subject: [PATCH 204/311] Update sbt, scripted-plugin to 1.10.10 (#1357) --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index 73df629ac..e97b27220 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.7 +sbt.version=1.10.10 diff --git a/project/build.properties b/project/build.properties index 73df629ac..e97b27220 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.7 +sbt.version=1.10.10 From 9f2437217b3d61bac4843705f0a35a9278014ba1 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 6 Mar 2025 09:23:20 +0100 Subject: [PATCH 205/311] Fix `ArrayIndexOutOfBoundsException` when decoding too long string as `java.time._` values (#1358) --- .../main/scala/zio/json/internal/lexer.scala | 354 +++++++----------- .../test/scala/zio/json/JavaTimeSpec.scala | 170 +++++---- 2 files changed, 228 insertions(+), 296 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 5c7bec203..60dee07a6 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -299,69 +299,71 @@ object Lexer { def uuid(trace: List[JsonError], in: OneCharReader): UUID = { var c = in.nextNonWhitespace() if (c == '"') { - val cs = charArrays.get - var i = 0 + val cs = charArrays.get + var i, m = 0 while ({ c = in.readChar() - c != '"' + i < 1024 && c != '"' }) { if (c == '\\') c = nextEscaped(trace, in) - if (i == 36 || c > 0xff) uuidError(trace) cs(i) = c + m |= c i += 1 } - if ( - i == 36 && { - val c1 = cs(8) - val c2 = cs(13) - val c3 = cs(18) - val c4 = cs(23) - c1 == '-' && c2 == '-' && c3 == '-' && c4 == '-' - } - ) { - val ds = hexDigits - val msb1 = - ds(cs(0).toInt).toLong << 28 | - (ds(cs(1).toInt) << 24 | - ds(cs(2).toInt) << 20 | - ds(cs(3).toInt) << 16 | - ds(cs(4).toInt) << 12 | - ds(cs(5).toInt) << 8 | - ds(cs(6).toInt) << 4 | - ds(cs(7).toInt)) - val msb2 = - (ds(cs(9).toInt) << 12 | - ds(cs(10).toInt) << 8 | - ds(cs(11).toInt) << 4 | - ds(cs(12).toInt)).toLong - val msb3 = - (ds(cs(14).toInt) << 12 | - ds(cs(15).toInt) << 8 | - ds(cs(16).toInt) << 4 | - ds(cs(17).toInt)).toLong - val lsb1 = - (ds(cs(19).toInt) << 12 | - ds(cs(20).toInt) << 8 | - ds(cs(21).toInt) << 4 | - ds(cs(22).toInt)).toLong - val lsb2 = - (ds(cs(24).toInt) << 16 | - ds(cs(25).toInt) << 12 | - ds(cs(26).toInt) << 8 | - ds(cs(27).toInt) << 4 | - ds(cs(28).toInt)).toLong << 28 | - (ds(cs(29).toInt) << 24 | - ds(cs(30).toInt) << 20 | - ds(cs(31).toInt) << 16 | - ds(cs(32).toInt) << 12 | - ds(cs(33).toInt) << 8 | - ds(cs(34).toInt) << 4 | - ds(cs(35).toInt)) - if ((msb1 | msb2 | msb3 | lsb1 | lsb2) >= 0L) { - return new UUID(msb1 << 32 | msb2 << 16 | msb3, lsb1 << 48 | lsb2) + if (m <= 0xff) { + if ( + i == 36 && { + val c1 = cs(8) + val c2 = cs(13) + val c3 = cs(18) + val c4 = cs(23) + c1 == '-' && c2 == '-' && c3 == '-' && c4 == '-' + } + ) { + val ds = hexDigits + val msb1 = + ds(cs(0)).toLong << 28 | + (ds(cs(1)) << 24 | + ds(cs(2)) << 20 | + ds(cs(3)) << 16 | + ds(cs(4)) << 12 | + ds(cs(5)) << 8 | + ds(cs(6)) << 4 | + ds(cs(7))) + val msb2 = + (ds(cs(9)) << 12 | + ds(cs(10)) << 8 | + ds(cs(11)) << 4 | + ds(cs(12))).toLong + val msb3 = + (ds(cs(14)) << 12 | + ds(cs(15)) << 8 | + ds(cs(16)) << 4 | + ds(cs(17))).toLong + val lsb1 = + (ds(cs(19)) << 12 | + ds(cs(20)) << 8 | + ds(cs(21)) << 4 | + ds(cs(22))).toLong + val lsb2 = + (ds(cs(24)) << 16 | + ds(cs(25)) << 12 | + ds(cs(26)) << 8 | + ds(cs(27)) << 4 | + ds(cs(28))).toLong << 28 | + (ds(cs(29)) << 24 | + ds(cs(30)) << 20 | + ds(cs(31)) << 16 | + ds(cs(32)) << 12 | + ds(cs(33)) << 8 | + ds(cs(34)) << 4 | + ds(cs(35))) + if ((msb1 | msb2 | msb3 | lsb1 | lsb2) >= 0L) { + return new UUID(msb1 << 32 | msb2 << 16 | msb3, lsb1 << 48 | lsb2) + } + } else if (i <= 36) { + return uuidExtended(trace, cs, i) } - } else if (i <= 36) { - return uuidExtended(trace, cs, i) } } uuidError(trace) @@ -405,7 +407,7 @@ object Lexer { var result = 0L var i = from while (i < to) { - result = (result << 4) | ds(cs(i).toInt) + result = (result << 4) | ds(cs(i)) i += 1 } if ((result & mask) == 0L) return result @@ -414,21 +416,11 @@ object Lexer { } def duration(trace: List[JsonError], in: OneCharReader): Duration = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } - var pos = 0 - var seconds = 0L - var nanos, state = 0 + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) + var seconds = 0L + var nanos, pos, state = 0 if (pos >= i) durationError(trace) var ch = cs(pos) pos += 1 @@ -526,18 +518,9 @@ object Lexer { } def instant(trace: List[JsonError], in: OneCharReader): Instant = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos, year, month, day = 0 if ( pos + 4 >= i || { @@ -697,20 +680,12 @@ object Lexer { } def localDate(trace: List[JsonError], in: OneCharReader): LocalDate = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } - var pos, year, month, day = 0 - if ( + var year, month, day = 0 + if ( + in.nextNonWhitespace() != '"' || { + val cs = charArrays.get + val i = readChars(trace, in, cs) + var pos = 0 pos + 4 >= i || { val ch0 = cs(pos) val ch1 = cs(pos + 1) @@ -758,25 +733,15 @@ object Lexer { ch4 < '0' || ch4 > '9' || month < 1 || month > 12 || day == 0 || (day > 28 && day > maxDayForYearMonth(year, month)) } - ) localDateError(trace) - return LocalDate.of(year, month, day) - } - localDateError(trace) + } + ) localDateError(trace) + LocalDate.of(year, month, day) } def localDateTime(trace: List[JsonError], in: OneCharReader): LocalDateTime = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos, year, month, day = 0 if ( pos + 4 >= i || { @@ -884,18 +849,9 @@ object Lexer { } def localTime(trace: List[JsonError], in: OneCharReader): LocalTime = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos, hour, minute = 0 if ( pos + 4 >= i || { @@ -952,20 +908,11 @@ object Lexer { } def monthDay(trace: List[JsonError], in: OneCharReader): MonthDay = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } - var month, day = 0 - if ( + var month, day = 0 + if ( + in.nextNonWhitespace() != '"' || { + val cs = charArrays.get + val i = readChars(trace, in, cs) i != 7 || { val ch0 = cs(0) val ch1 = cs(1) @@ -980,25 +927,15 @@ object Lexer { ch5 < '0' || ch5 > '9' || ch6 < '0' || ch6 > '9' || month < 1 || month > 12 || day == 0 || (day > 28 && day > maxDayForMonth(month)) } - ) monthDayError(trace) - return MonthDay.of(month, day) - } - monthDayError(trace) + } + ) monthDayError(trace) + MonthDay.of(month, day) } def offsetDateTime(trace: List[JsonError], in: OneCharReader): OffsetDateTime = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos, year, month, day = 0 if ( pos + 4 >= i || { @@ -1144,18 +1081,9 @@ object Lexer { } def offsetTime(trace: List[JsonError], in: OneCharReader): OffsetTime = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos, hour, minute = 0 if ( pos + 4 >= i || { @@ -1250,18 +1178,9 @@ object Lexer { } def period(trace: List[JsonError], in: OneCharReader): Period = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos, state, years, months, days = 0 if (pos >= i) periodError(trace) var ch = cs(pos) @@ -1332,18 +1251,9 @@ object Lexer { } def year(trace: List[JsonError], in: OneCharReader): Year = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos, year = 0 if ( pos + 3 >= i || { @@ -1385,18 +1295,9 @@ object Lexer { } def yearMonth(trace: List[JsonError], in: OneCharReader): YearMonth = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos, year, month = 0 if ( pos + 4 >= i || { @@ -1446,18 +1347,9 @@ object Lexer { } def zonedDateTime(trace: List[JsonError], in: OneCharReader): ZonedDateTime = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos, year, month, day, hour, minute = 0 if ( pos + 4 >= i || { @@ -1638,18 +1530,9 @@ object Lexer { } def zoneOffset(trace: List[JsonError], in: OneCharReader): ZoneOffset = { - var c = in.nextNonWhitespace() - if (c == '"') { - val cs = charArrays.get - var i = 0 - while ({ - c = in.readChar() - c != '"' - }) { - if (c == '\\') c = nextEscaped(trace, in) - cs(i) = c - i += 1 - } + if (in.nextNonWhitespace() == '"') { + val cs = charArrays.get + val i = readChars(trace, in, cs) var pos = 0 if (pos >= i) zoneOffsetError(trace) val ch = cs(pos) @@ -1702,6 +1585,21 @@ object Lexer { zoneOffsetError(trace) } + private[this] def readChars(trace: List[JsonError], in: OneCharReader, cs: Array[Char]): Int = { + val len = cs.length + var c = '0' + var i = 0 + while ({ + c = in.readChar() + i < len && c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + cs(i) = c + i += 1 + } + i + } + private[this] def toZoneOffset(offsetNeg: Boolean, offsetTotal: Int): ZoneOffset = { var qp = offsetTotal * 37283 if ((qp & 0x1ff8000) == 0) { // check if offsetTotal divisible by 900 @@ -1785,7 +1683,7 @@ object Lexer { @noinline private[this] def zoneOffsetError(trace: List[JsonError]): Nothing = error("expected a ZoneOffset", trace) private[this] val charArrays = new ThreadLocal[Array[Char]] { - override def initialValue(): Array[Char] = new Array[Char](1024) // should be longer than 256 + override def initialValue(): Array[Char] = new Array[Char](1024) } private[this] val hexDigits: Array[Byte] = { diff --git a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala index bcd81eaa5..27e9553f2 100644 --- a/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/JavaTimeSpec.scala @@ -277,6 +277,7 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("-PT24H").fromJson[Duration])(isRight(equalTo(Duration.ofHours(-24)))) && assert(stringify("P1D").fromJson[Duration])(isRight(equalTo(Duration.ofHours(24)))) && assert(stringify("P1DT0H").fromJson[Duration])(isRight(equalTo(Duration.ofHours(24)))) && + assert(stringify("P1DT0\\u0048").fromJson[Duration])(isRight(equalTo(Duration.ofHours(24)))) && assert(stringify("PT2562047788015215H30M7.999999999S").fromJson[Duration])( isRight(equalTo(Duration.ofSeconds(Long.MaxValue, 999999999L))) ) @@ -286,6 +287,7 @@ object JavaTimeSpec extends ZIOSpecDefault { val p = n.toInstant assert(stringify("1970-01-01T00:00:00Z").fromJson[Instant])(isRight(equalTo(Instant.EPOCH))) && assert(stringify("1970-01-01T00:00:00.Z").fromJson[Instant])(isRight(equalTo(Instant.EPOCH))) && + assert(stringify("1970-01-01T00:00:00.\\u005a").fromJson[Instant])(isRight(equalTo(Instant.EPOCH))) && assert(stringify(p).fromJson[Instant])(isRight(equalTo(p))) && assert(stringify(n).fromJson[Instant])(isRight(equalTo(p))) }, @@ -301,7 +303,8 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify(n).fromJson[LocalDateTime])(isRight(equalTo(n))) && assert(stringify("2020-01-01T12:36").fromJson[LocalDateTime])(isRight(equalTo(p))) && assert(stringify("2020-01-01T12:36:00").fromJson[LocalDateTime])(isRight(equalTo(p))) && - assert(stringify("2020-01-01T12:36:00.").fromJson[LocalDateTime])(isRight(equalTo(p))) + assert(stringify("2020-01-01T12:36:00.").fromJson[LocalDateTime])(isRight(equalTo(p))) && + assert(stringify("2020-01-01T12:36:00\\u002e").fromJson[LocalDateTime])(isRight(equalTo(p))) }, test("LocalTime") { val n = LocalTime.now() @@ -309,7 +312,8 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify(n).fromJson[LocalTime])(isRight(equalTo(n))) && assert(stringify("12:36").fromJson[LocalTime])(isRight(equalTo(p))) && assert(stringify("12:36:00").fromJson[LocalTime])(isRight(equalTo(p))) && - assert(stringify("12:36:00.").fromJson[LocalTime])(isRight(equalTo(p))) + assert(stringify("12:36:00.").fromJson[LocalTime])(isRight(equalTo(p))) && + assert(stringify("12:36:00\\u002e").fromJson[LocalTime])(isRight(equalTo(p))) }, test("Month fromJson") { assert(stringify("JANUARY").fromJson[Month])(isRight(equalTo(Month.JANUARY))) && @@ -351,21 +355,24 @@ object JavaTimeSpec extends ZIOSpecDefault { val n = MonthDay.now() val p = MonthDay.of(1, 1) assert(stringify(n).fromJson[MonthDay])(isRight(equalTo(n))) && - assert(stringify("--01-01").fromJson[MonthDay])(isRight(equalTo(p))) + assert(stringify("--01-01").fromJson[MonthDay])(isRight(equalTo(p))) && + assert(stringify("\\u002d-01-01").fromJson[MonthDay])(isRight(equalTo(p))) }, test("OffsetDateTime") { val n = OffsetDateTime.now() val p = OffsetDateTime.of(2020, 1, 1, 12, 36, 12, 0, ZoneOffset.UTC) assert(stringify(n).fromJson[OffsetDateTime])(isRight(equalTo(n))) && assert(stringify("2020-01-01T12:36:12Z").fromJson[OffsetDateTime])(isRight(equalTo(p))) && - assert(stringify("2020-01-01T12:36:12.Z").fromJson[OffsetDateTime])(isRight(equalTo(p))) + assert(stringify("2020-01-01T12:36:12.Z").fromJson[OffsetDateTime])(isRight(equalTo(p))) && + assert(stringify("2020-01-01T12:36:12.\\u005a").fromJson[OffsetDateTime])(isRight(equalTo(p))) }, test("OffsetTime") { val n = OffsetTime.now() val p = OffsetTime.of(12, 36, 12, 0, ZoneOffset.ofHours(-4)) assert(stringify(n).fromJson[OffsetTime])(isRight(equalTo(n))) && assert(stringify("12:36:12-04:00").fromJson[OffsetTime])(isRight(equalTo(p))) && - assert(stringify("12:36:12.-04:00").fromJson[OffsetTime])(isRight(equalTo(p))) + assert(stringify("12:36:12.-04:00").fromJson[OffsetTime])(isRight(equalTo(p))) && + assert(stringify("12:36:12\\u002e-04:00").fromJson[OffsetTime])(isRight(equalTo(p))) }, test("Period") { assert(stringify("P0D").fromJson[Period])(isRight(equalTo(Period.ZERO))) && @@ -374,7 +381,8 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("-P1D").fromJson[Period])(isRight(equalTo(Period.ofDays(-1)))) && assert(stringify("P2M").fromJson[Period])(isRight(equalTo(Period.ofMonths(2)))) && assert(stringify("P364D").fromJson[Period])(isRight(equalTo(Period.ofWeeks(52)))) && - assert(stringify("P10Y").fromJson[Period])(isRight(equalTo(Period.ofYears(10)))) + assert(stringify("P10Y").fromJson[Period])(isRight(equalTo(Period.ofYears(10)))) && + assert(stringify("P10\\u0059").fromJson[Period])(isRight(equalTo(Period.ofYears(10)))) }, test("Year") { val n = Year.now() @@ -386,7 +394,8 @@ object JavaTimeSpec extends ZIOSpecDefault { val n = YearMonth.now() assert(stringify(n).fromJson[YearMonth])(isRight(equalTo(n))) && assert(stringify("1999-12").fromJson[YearMonth])(isRight(equalTo(YearMonth.of(1999, 12)))) && - assert(stringify("1999-01").fromJson[YearMonth])(isRight(equalTo(YearMonth.of(1999, 1)))) + assert(stringify("1999-01").fromJson[YearMonth])(isRight(equalTo(YearMonth.of(1999, 1)))) && + assert(stringify("1999\\u002d01").fromJson[YearMonth])(isRight(equalTo(YearMonth.of(1999, 1)))) }, test("ZonedDateTime") { def zdtAssert(actual: String, expected: ZonedDateTime): TestResult = @@ -398,14 +407,9 @@ object JavaTimeSpec extends ZIOSpecDefault { val utc = ZonedDateTime.of(ld, ZoneId.of("Etc/UTC")) val gmt = ZonedDateTime.of(ld, ZoneId.of("+00:00")) - zdtAssert( - "+164433183-11-15T12:32:00.076988677Z[Atlantic/Madeira]", - OffsetDateTime - .parse("+164433183-11-15T12:32:00.076988677Z") - .atZoneSameInstant(ZoneId.of("Atlantic/Madeira")) - ) && zdtAssert(n.toString, n) && zdtAssert("2020-01-01T12:36:00-05:00[America/New_York]", est) && + zdtAssert("2020-01-01T12:36:00-05:00[America\\u002fNew_York]", est) && zdtAssert("2020-01-01T12:36:00Z[Etc/UTC]", utc) && zdtAssert("2020-01-01T12:36:00+00:00[+00:00]", gmt) && zdtAssert( @@ -468,28 +472,18 @@ object JavaTimeSpec extends ZIOSpecDefault { ) }, test("ZoneId") { - assert(stringify("America/New_York").fromJson[ZoneId])( - isRight( - equalTo( - ZoneId.of("America/New_York") - ) - ) + assert(stringify("America/New_York").fromJson[ZoneId])(isRight(equalTo(ZoneId.of("America/New_York")))) && + assert(stringify("America\\u002fNew_York").fromJson[ZoneId])( + isRight(equalTo(ZoneId.of("America/New_York"))) ) && assert(stringify("Etc/UTC").fromJson[ZoneId])(isRight(equalTo(ZoneId.of("Etc/UTC")))) && - assert(stringify("Pacific/Auckland").fromJson[ZoneId])( - isRight( - equalTo( - ZoneId.of("Pacific/Auckland") - ) - ) - ) && - assert(stringify("Asia/Shanghai").fromJson[ZoneId])( - isRight(equalTo(ZoneId.of("Asia/Shanghai"))) - ) && + assert(stringify("Pacific/Auckland").fromJson[ZoneId])(isRight(equalTo(ZoneId.of("Pacific/Auckland")))) && + assert(stringify("Asia/Shanghai").fromJson[ZoneId])(isRight(equalTo(ZoneId.of("Asia/Shanghai")))) && assert(stringify("Africa/Cairo").fromJson[ZoneId])(isRight(equalTo(ZoneId.of("Africa/Cairo")))) }, test("ZoneOffset") { assert(stringify("Z").fromJson[ZoneOffset])(isRight(equalTo(ZoneOffset.UTC))) && + assert(stringify("\\u005a").fromJson[ZoneOffset])(isRight(equalTo(ZoneOffset.UTC))) && assert(stringify("+05:00").fromJson[ZoneOffset])(isRight(equalTo(ZoneOffset.ofHours(5)))) && assert(stringify("-05:00").fromJson[ZoneOffset])(isRight(equalTo(ZoneOffset.ofHours(-5)))) && assert(stringify("+05:10:10").fromJson[ZoneOffset])( @@ -499,53 +493,79 @@ object JavaTimeSpec extends ZIOSpecDefault { ), suite("Decoder Sad Path")( test("Duration") { - assert("""""""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""X"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""-"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""-X"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""PXD"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P-"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P-XD"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1XD"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""PT"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""PT0SX"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P106751991167301D"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1067519911673000D"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P-106751991167301D"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DX1H"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DTXH"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT-XH"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT1XH"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT1H1XM"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT2562047788015216H"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT-2562047788015216H"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT153722867280912931M"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT-153722867280912931M"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT9223372036854775808S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT92233720368547758000S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT-9223372036854775809S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT1H1MXS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT1H1M-XS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT1H1M0XS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT1H1M0.XS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT1H1M0.012345678XS"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P1DT1H1M0.0123456789S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT0H0M9223372036854775808S"""".fromJson[Duration])( + assert(stringify(" " * 10000).fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("X").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("-").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("-X").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("PXD").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P-").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P-XD").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1XD").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("PT").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("PT0SX").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DT").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P106751991167301D").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1067519911673000D").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P-106751991167301D").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DX1H").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DTXH").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DT-XH").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DT1XH").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DT1H1XM").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P0DT2562047788015216H").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P0DT-2562047788015216H").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P0DT153722867280912931M").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P0DT-153722867280912931M").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P0DT9223372036854775808S").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P0DT92233720368547758000S").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P0DT-9223372036854775809S").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P1DT1H1MXS").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DT1H1M-XS").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DT1H1M0XS").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DT1H1M0.XS").fromJson[Duration])(isLeft(containsString("expected a Duration"))) && + assert(stringify("P1DT1H1M0.012345678XS").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P1DT1H1M0.0123456789S").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P0DT0H0M9223372036854775808S").fromJson[Duration])( isLeft(containsString("expected a Duration")) ) && - assert(""""P0DT0H0M92233720368547758080S"""".fromJson[Duration])( + assert(stringify("P0DT0H0M92233720368547758080S").fromJson[Duration])( isLeft(containsString("expected a Duration")) ) && - assert(""""P0DT0H0M-9223372036854775809S"""".fromJson[Duration])( + assert(stringify("P0DT0H0M-9223372036854775809S").fromJson[Duration])( isLeft(containsString("expected a Duration")) ) && - assert(""""P106751991167300DT24H"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT2562047788015215H60M"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) && - assert(""""P0DT0H153722867280912930M60S"""".fromJson[Duration])(isLeft(containsString("expected a Duration"))) + assert(stringify("P106751991167300DT24H").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P0DT2562047788015215H60M").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) && + assert(stringify("P0DT0H153722867280912930M60S").fromJson[Duration])( + isLeft(containsString("expected a Duration")) + ) }, test("Instant") { + assert(stringify(" " * 10000).fromJson[Instant])(isLeft(containsString("expected an Instant"))) && assert(stringify("").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && assert(stringify("2020").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && assert(stringify("2020-0").fromJson[Instant])(isLeft(containsString("expected an Instant"))) && @@ -659,6 +679,7 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("2020-12-32T01:01Z").fromJson[Instant])(isLeft(containsString("expected an Instant"))) }, test("LocalDate") { + assert(stringify(" " * 10000).fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && assert(stringify("").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && assert(stringify("2020").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && assert(stringify("2020-0").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) && @@ -703,6 +724,7 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("2020-12-32").fromJson[LocalDate])(isLeft(containsString("expected a LocalDate"))) }, test("LocalDateTime") { + assert(stringify(" " * 10000).fromJson[LocalDateTime])(isLeft(containsString("expected a LocalDateTime"))) && assert(stringify("").fromJson[LocalDateTime])(isLeft(containsString("expected a LocalDateTime"))) && assert(stringify("2020").fromJson[LocalDateTime])(isLeft(containsString("expected a LocalDateTime"))) && assert(stringify("2020-0").fromJson[LocalDateTime])(isLeft(containsString("expected a LocalDateTime"))) && @@ -869,6 +891,7 @@ object JavaTimeSpec extends ZIOSpecDefault { ) }, test("LocalTime") { + assert(stringify(" " * 10000).fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && assert(stringify("").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && assert(stringify("0").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && assert(stringify("01:0").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) && @@ -888,6 +911,7 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("01:01:01.X").fromJson[LocalTime])(isLeft(containsString("expected a LocalTime"))) }, test("MonthDay") { + assert(stringify(" " * 10000).fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && assert(stringify("").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && assert(stringify("X-01-01").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && assert(stringify("-X01-01").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) && @@ -913,6 +937,9 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("--12-32").fromJson[MonthDay])(isLeft(containsString("expected a MonthDay"))) }, test("OffsetDateTime") { + assert(stringify(" " * 10000).fromJson[OffsetDateTime])( + isLeft(containsString("expected an OffsetDateTime")) + ) && assert(stringify("").fromJson[OffsetDateTime])(isLeft(containsString("expected an OffsetDateTime"))) && assert(stringify("2020").fromJson[OffsetDateTime])(isLeft(containsString("expected an OffsetDateTime"))) && assert(stringify("2020-0").fromJson[OffsetDateTime])(isLeft(containsString("expected an OffsetDateTime"))) && @@ -1141,6 +1168,7 @@ object JavaTimeSpec extends ZIOSpecDefault { ) }, test("OffsetTime") { + assert(stringify(" " * 10000).fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && assert(stringify("").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && assert(stringify("0").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && assert(stringify("01:0").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) && @@ -1205,6 +1233,7 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("01:01:01+01:01:60").fromJson[OffsetTime])(isLeft(containsString("expected an OffsetTime"))) }, test("Period") { + assert(stringify(" " * 10000).fromJson[Period])(isLeft(containsString("expected a Period"))) && assert(stringify("").fromJson[Period])(isLeft(containsString("expected a Period"))) && assert(stringify("X").fromJson[Period])(isLeft(containsString("expected a Period"))) && assert(stringify("P").fromJson[Period])(isLeft(containsString("expected a Period"))) && @@ -1259,6 +1288,7 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("P1Y1M1W1DX").fromJson[Period])(isLeft(containsString("expected a Period"))) }, test("Year") { + assert(stringify(" " * 10000).fromJson[Year])(isLeft(containsString("expected a Year"))) && assert(stringify("").fromJson[Year])(isLeft(containsString("expected a Year"))) && assert(stringify("2").fromJson[Year])(isLeft(containsString("expected a Year"))) && assert(stringify("22").fromJson[Year])(isLeft(containsString("expected a Year"))) && @@ -1281,6 +1311,7 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("10000").fromJson[Year])(isLeft(containsString("expected a Year"))) }, test("YearMonth") { + assert(stringify(" " * 10000).fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && assert(stringify("").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && assert(stringify("2020").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && assert(stringify("2020-0").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) && @@ -1308,6 +1339,7 @@ object JavaTimeSpec extends ZIOSpecDefault { assert(stringify("2020-13").fromJson[YearMonth])(isLeft(containsString("expected a YearMonth"))) }, test("ZonedDateTime") { + assert(stringify(" " * 10000).fromJson[ZonedDateTime])(isLeft(containsString("expected a ZonedDateTime"))) && assert(stringify("").fromJson[ZonedDateTime])(isLeft(containsString("expected a ZonedDateTime"))) && assert(stringify("2020").fromJson[ZonedDateTime])(isLeft(containsString("expected a ZonedDateTime"))) && assert(stringify("2020-0").fromJson[ZonedDateTime])(isLeft(containsString("expected a ZonedDateTime"))) && @@ -1543,10 +1575,12 @@ object JavaTimeSpec extends ZIOSpecDefault { ) }, test("ZoneId") { + assert(stringify(" " * 10000).fromJson[ZoneId])(isLeft(containsString("expected a ZoneId"))) && assert(stringify("America/New York").fromJson[ZoneId])(isLeft(containsString("expected a ZoneId"))) && assert(stringify("Solar_System/Mars").fromJson[ZoneId])(isLeft(containsString("expected a ZoneId"))) }, test("ZoneOffset") { + assert(stringify(" " * 10000).fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && assert(stringify("").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && assert(stringify("X").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && assert(stringify("+X1:01:01").fromJson[ZoneOffset])(isLeft(containsString("expected a ZoneOffset"))) && From e801cfcb9556049c09864b7229612f73c6f716f3 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:33:11 +0100 Subject: [PATCH 206/311] Update scalafmt-core to 3.9.3 (#1360) --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index e7cfc5922..752621c87 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.9.2" +version = "3.9.3" runner.dialect = scala213 maxColumn = 120 align.preset = most From 12f904b11e9b385ebf0ed2a3261722b402c8e9f5 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:33:41 +0100 Subject: [PATCH 207/311] Update zio-interop-cats to 23.1.0.4 (#1359) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index ef0805861..9648e5cce 100644 --- a/build.sbt +++ b/build.sbt @@ -325,7 +325,7 @@ lazy val zioJsonInteropHttp4s = project "org.http4s" %% "http4s-dsl" % "0.23.30", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.5.7", - "dev.zio" %% "zio-interop-cats" % "23.1.0.3" % "test", + "dev.zio" %% "zio-interop-cats" % "23.1.0.4" % "test", "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" ), From 28269533d74edac62b03eb1d6c1bdb95eedd2f09 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 14 Mar 2025 06:44:24 +0100 Subject: [PATCH 208/311] Update scalafmt-core to 3.9.4 (#1362) --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 752621c87..a16232c42 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.9.3" +version = "3.9.4" runner.dialect = scala213 maxColumn = 120 align.preset = most From d44e398571d2787bc19bec873a4de5e8f8e22a83 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 14 Mar 2025 07:52:11 +0100 Subject: [PATCH 209/311] Update sbt-ci-release to 1.9.3 (#1361) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a5f13021b..f66d251ac 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.2") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.3") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") From 4bce205a06819a69bfa814e762fad74cf91f581b Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:22:41 +0100 Subject: [PATCH 210/311] Update zio-interop-cats to 23.1.0.5 (#1367) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 9648e5cce..79db51fcd 100644 --- a/build.sbt +++ b/build.sbt @@ -325,7 +325,7 @@ lazy val zioJsonInteropHttp4s = project "org.http4s" %% "http4s-dsl" % "0.23.30", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.5.7", - "dev.zio" %% "zio-interop-cats" % "23.1.0.4" % "test", + "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % "test", "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" ), From b390ea1aac3627d7f429f6b1817e3dc2d2da553f Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Wed, 26 Mar 2025 20:31:44 +0100 Subject: [PATCH 211/311] Update sbt, scripted-plugin to 1.10.11 (#1365) --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index e97b27220..cc68b53f1 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.10 +sbt.version=1.10.11 diff --git a/project/build.properties b/project/build.properties index e97b27220..cc68b53f1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.10 +sbt.version=1.10.11 From 80a7b5f04f9d3f83b3c7f8212d6533a98ee56612 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Tue, 1 Apr 2025 07:48:51 +0200 Subject: [PATCH 212/311] Update cats-effect to 3.6.0 (#1370) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 79db51fcd..94ce9932c 100644 --- a/build.sbt +++ b/build.sbt @@ -324,7 +324,7 @@ lazy val zioJsonInteropHttp4s = project libraryDependencies ++= Seq( "org.http4s" %% "http4s-dsl" % "0.23.30", "dev.zio" %% "zio" % zioVersion, - "org.typelevel" %% "cats-effect" % "3.5.7", + "org.typelevel" %% "cats-effect" % "3.6.0", "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % "test", "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" From 5a47bf21106c147973b29aadfb10f09efc63ecda Mon Sep 17 00:00:00 2001 From: Vladimir Klyushnikov <72238+vladimirkl@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:59:15 +0300 Subject: [PATCH 213/311] Fix error message for invalid boolean (#1371) --- zio-json/shared/src/main/scala/zio/json/internal/lexer.scala | 2 +- zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 60dee07a6..9e7cb7952 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -153,7 +153,7 @@ object Lexer { if (c == 't' && in.readChar() == 'r' && in.readChar() == 'u' && in.readChar() == 'e') true else if (c == 'f' && in.readChar() == 'a' && in.readChar() == 'l' && in.readChar() == 's' && in.readChar() == 'e') false - else error("expected a Boolean", c, trace) + else error("expected a Boolean", trace) } def byte(trace: List[JsonError], in: RetractReader): Byte = diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index c7f62b44b..e3f72cee3 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -34,6 +34,11 @@ object DecoderSpec extends ZIOSpecDefault { assert("\"\u0000\"".fromJson[Char])(isLeft(equalTo("""(invalid control in string)"""))) && assert("\"\\u0000\"".replace('0', 'g').fromJson[Char])(isLeft(equalTo("""(invalid charcode in string)"""))) }, + test("boolean") { + assert("true".fromJson[Boolean])(isRight(equalTo(true))) && + assert("false".fromJson[Boolean])(isRight(equalTo(false))) && + assert("x".fromJson[Boolean])(isLeft(equalTo("(expected a Boolean)"))) + }, test("byte") { assert("-128".fromJson[Byte])(isRight(equalTo(Byte.MinValue))) && assert("127".fromJson[Byte])(isRight(equalTo(Byte.MaxValue))) && From 4602cc49029ba4276d49e1c7eecce1b61c3c16d6 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Wed, 9 Apr 2025 06:59:48 +0200 Subject: [PATCH 214/311] Update zio, zio-streams, zio-test, ... to 2.1.17 (#1372) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 94ce9932c..d6a01dc3c 100644 --- a/build.sbt +++ b/build.sbt @@ -58,7 +58,7 @@ addCommandAlias( "zioJsonMacrosNative/test" ) -val zioVersion = "2.1.16" +val zioVersion = "2.1.17" lazy val zioJsonRoot = project .in(file(".")) From 8f051e193339274eba8b43c4fcbf2c9e03c2105c Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Wed, 9 Apr 2025 07:00:02 +0200 Subject: [PATCH 215/311] Update cats-effect to 3.6.1 (#1373) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d6a01dc3c..f9c8f799b 100644 --- a/build.sbt +++ b/build.sbt @@ -324,7 +324,7 @@ lazy val zioJsonInteropHttp4s = project libraryDependencies ++= Seq( "org.http4s" %% "http4s-dsl" % "0.23.30", "dev.zio" %% "zio" % zioVersion, - "org.typelevel" %% "cats-effect" % "3.6.0", + "org.typelevel" %% "cats-effect" % "3.6.1", "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % "test", "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" From 83a8a69bc62df09ff9a47d7901998b8635d50459 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Wed, 9 Apr 2025 15:50:53 +0200 Subject: [PATCH 216/311] Add support up to 128 cases in sum types and up to 128 fields in product types (#1374) --- build.sbt | 2 +- .../src/main/scala-2.x/zio/json/macros.scala | 237 ++++++++++++------ .../src/main/scala-3/zio/json/macros.scala | 232 ++++++++++++----- .../main/scala/zio/json/internal/lexer.scala | 26 ++ .../src/test/scala/zio/json/DecoderSpec.scala | 117 +++++++++ 5 files changed, 473 insertions(+), 141 deletions(-) diff --git a/build.sbt b/build.sbt index f9c8f799b..cf18527cf 100644 --- a/build.sbt +++ b/build.sbt @@ -102,7 +102,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) scalacOptions -= "-opt-inline-from:zio.internal.**", Test / scalacOptions ++= { if (scalaVersion.value == ScalaDotty) - Vector("-Yretain-trees", "-Xmax-inlines:100") + Vector("-Yretain-trees", "-Xmax-inlines:128") else Vector.empty }, diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index db0ef8d11..184f32933 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -392,7 +392,11 @@ object DeriveJsonDecoder { s"name(s) ${collisions.mkString(",")} are duplicated" ) } - val matrix = new StringMatrix(names) + val (names1, names2) = names.splitAt(64) + val matrix1 = new StringMatrix(names1) + val matrix2 = + if (names2.isEmpty) null + else new StringMatrix(names2) lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap = names.zipWithIndex.toMap val discrim = @@ -400,90 +404,181 @@ object DeriveJsonDecoder { lazy val isEnumeration = config.enumValuesAsStrings && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[JsonDecoder, _]]) if (discrim.isEmpty && isEnumeration) { - new JsonDecoder[A] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - val idx = Lexer.enumeration(trace, in, matrix) - if (idx >= 0) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - else Lexer.error("invalid enumeration value", trace) - } + if (names.length <= 64) { + new JsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val idx = Lexer.enumeration(trace, in, matrix1) + if (idx >= 0) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + else Lexer.error("invalid enumeration value", trace) + } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case s: Json.Str => - namesMap.get(s.value) match { - case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - case _ => Lexer.error("invalid enumeration value", trace) - } - case _ => Lexer.error("expected string", trace) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case s: Json.Str => + namesMap.get(s.value) match { + case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + case _ => Lexer.error("invalid enumeration value", trace) + } + case _ => Lexer.error("expected string", trace) + } + } + } else { + new JsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val idx = Lexer.enumeration128(trace, in, matrix1, matrix2) + if (idx >= 0) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + else Lexer.error("invalid enumeration value", trace) } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case s: Json.Str => + namesMap.get(s.value) match { + case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + case _ => Lexer.error("invalid enumeration value", trace) + } + case _ => Lexer.error("expected string", trace) + } + } } } else if (discrim.isEmpty) { // We're not allowing extra fields in this encoding - new JsonDecoder[A] { - private[this] val spans = names.map(JsonError.ObjectAccess) + if (names.length <= 64) { + new JsonDecoder[A] { + private[this] val spans = names.map(JsonError.ObjectAccess) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + Lexer.char(trace, in, '{') + if (Lexer.firstField(trace, in)) { + val idx = Lexer.field(trace, in, matrix1) + if (idx >= 0) { + val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] + Lexer.char(trace, in, '}') + a + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.error("expected non-empty object", trace) + } - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - Lexer.char(trace, in, '{') - if (Lexer.firstField(trace, in)) { - val idx = Lexer.field(trace, in, matrix) - if (idx >= 0) { - val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] - Lexer.char(trace, in, '}') - a - } else Lexer.error("invalid disambiguator", trace) - } else Lexer.error("expected non-empty object", trace) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj if o.fields.length == 1 => + val kv = o.fields(0) + namesMap.get(kv._1) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) + } + case _ => Lexer.error("expected single field object", trace) + } } + } else { + new JsonDecoder[A] { + private[this] val spans = names.map(JsonError.ObjectAccess) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case o: Json.Obj if o.fields.length == 1 => - val kv = o.fields(0) - namesMap.get(kv._1) match { - case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2).asInstanceOf[A] - case _ => Lexer.error("invalid disambiguator", trace) - } - case _ => Lexer.error("expected single field object", trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + Lexer.char(trace, in, '{') + if (Lexer.firstField(trace, in)) { + val idx = Lexer.field128(trace, in, matrix1, matrix2) + if (idx >= 0) { + val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] + Lexer.char(trace, in, '}') + a + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.error("expected non-empty object", trace) } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj if o.fields.length == 1 => + val kv = o.fields(0) + namesMap.get(kv._1) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) + } + case _ => Lexer.error("expected single field object", trace) + } + } } } else { - new JsonDecoder[A] { - private[this] val hintfield = discrim.get - private[this] val hintmatrix = new StringMatrix(Array(hintfield)) - private[this] val spans = names.map(JsonError.Message) - - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - val in_ = RecordingReader(in) - Lexer.char(trace, in_, '{') - if (Lexer.firstField(trace, in_)) { - do { - if (Lexer.field(trace, in_, hintmatrix) >= 0) { - val idx = Lexer.enumeration(trace, in_, matrix) - if (idx >= 0) { - in_.rewind() - return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] - } else Lexer.error("invalid disambiguator", trace) - } else Lexer.skipValue(trace, in_) - } while (Lexer.nextField(trace, in_)) + if (names.length <= 64) { + new JsonDecoder[A] { + private[this] val hintfield = discrim.get + private[this] val hintmatrix = new StringMatrix(Array(hintfield)) + private[this] val spans = names.map(JsonError.Message) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val in_ = RecordingReader(in) + Lexer.char(trace, in_, '{') + if (Lexer.firstField(trace, in_)) { + do { + if (Lexer.field(trace, in_, hintmatrix) >= 0) { + val idx = Lexer.enumeration(trace, in_, matrix1) + if (idx >= 0) { + in_.rewind() + return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.skipValue(trace, in_) + } while (Lexer.nextField(trace, in_)) + } + Lexer.error(s"missing hint '$hintfield'", trace) } - Lexer.error(s"missing hint '$hintfield'", trace) - } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case o: Json.Obj => - o.fields.collectFirst { - case kv if kv._1 == hintfield && kv._2.isInstanceOf[Json.Str] => - kv._2.asInstanceOf[Json.Str].value - } match { - case Some(name) => - namesMap.get(name) match { - case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] - case _ => Lexer.error("invalid disambiguator", trace) - } - case _ => Lexer.error(s"missing hint '$hintfield'", trace) - } - case _ => Lexer.error("expected object", trace) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj => + o.fields.collectFirst { + case kv if kv._1 == hintfield && kv._2.isInstanceOf[Json.Str] => + kv._2.asInstanceOf[Json.Str].value + } match { + case Some(name) => + namesMap.get(name) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) + } + case _ => Lexer.error(s"missing hint '$hintfield'", trace) + } + case _ => Lexer.error("expected object", trace) + } + } + } else { + new JsonDecoder[A] { + private[this] val hintfield = discrim.get + private[this] val hintmatrix = new StringMatrix(Array(hintfield)) + private[this] val spans = names.map(JsonError.Message) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val in_ = RecordingReader(in) + Lexer.char(trace, in_, '{') + if (Lexer.firstField(trace, in_)) { + do { + if (Lexer.field(trace, in_, hintmatrix) >= 0) { + val idx = Lexer.enumeration128(trace, in_, matrix1, matrix2) + if (idx >= 0) { + in_.rewind() + return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.skipValue(trace, in_) + } while (Lexer.nextField(trace, in_)) + } + Lexer.error(s"missing hint '$hintfield'", trace) } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj => + o.fields.collectFirst { + case kv if kv._1 == hintfield && kv._2.isInstanceOf[Json.Str] => + kv._2.asInstanceOf[Json.Str].value + } match { + case Some(name) => + namesMap.get(name) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) + } + case _ => Lexer.error(s"missing hint '$hintfield'", trace) + } + case _ => Lexer.error("expected object", trace) + } + } } } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 0aef5df14..f80555d45 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -391,7 +391,11 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv throw new AssertionError(s"Case names in ADT ${ctx.typeInfo.full} must be distinct, " + s"name(s) ${collisions.mkString(",")} are duplicated") } - val matrix: StringMatrix = new StringMatrix(names) + val (names1, names2) = names.splitAt(64) + val matrix1 = new StringMatrix(names1) + val matrix2 = + if (names2.isEmpty) null + else new StringMatrix(names2) lazy val tcs: Array[JsonDecoder[Any]] = IArray.genericWrapArray(ctx.subtypes.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap: Map[String, Int] = names.zipWithIndex.toMap @@ -401,89 +405,179 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv (ctx.isEnum && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[?, ?]]) || !ctx.isEnum && ctx.subtypes.forall(_.isObject)) if (discrim.isEmpty && isEnumeration) { - new JsonDecoder[A] { - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - val idx = Lexer.enumeration(trace, in, matrix) - if (idx >= 0) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - else Lexer.error("invalid enumeration value", trace) - } + if (names.length <= 64) { + new JsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val idx = Lexer.enumeration(trace, in, matrix1) + if (idx >= 0) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + else Lexer.error("invalid enumeration value", trace) + } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case s: Json.Str => namesMap.get(s.value) match { - case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) - case _ => Lexer.error("invalid enumeration value", trace) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case s: Json.Str => namesMap.get(s.value) match { + case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + case _ => Lexer.error("invalid enumeration value", trace) + } + case _ => Lexer.error("expected string", trace) } - case _ => Lexer.error("expected string", trace) + } + } else { + new JsonDecoder[A] { + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val idx = Lexer.enumeration128(trace, in, matrix1, matrix2) + if (idx >= 0) tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + else Lexer.error("invalid enumeration value", trace) } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case s: Json.Str => namesMap.get(s.value) match { + case Some(idx) => tcs(idx).asInstanceOf[CaseObjectDecoder[JsonDecoder, A]].ctx.rawConstruct(Nil) + case _ => Lexer.error("invalid enumeration value", trace) + } + case _ => Lexer.error("expected string", trace) + } + } } } else if (discrim.isEmpty) { // We're not allowing extra fields in this encoding - new JsonDecoder[A] { - private val spans = names.map(JsonError.ObjectAccess(_)) + if (names.length <= 64) { + new JsonDecoder[A] { + private val spans = names.map(JsonError.ObjectAccess(_)) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + Lexer.char(trace, in, '{') + if (Lexer.firstField(trace, in)) { + val idx = Lexer.field(trace, in, matrix1) + if (idx >= 0) { + val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] + Lexer.char(trace, in, '}') + a + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.error("expected non-empty object", trace) + } - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - Lexer.char(trace, in, '{') - if (Lexer.firstField(trace, in)) { - val idx = Lexer.field(trace, in, matrix) - if (idx >= 0) { - val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] - Lexer.char(trace, in, '}') - a - } else Lexer.error("invalid disambiguator", trace) - } else Lexer.error("expected non-empty object", trace) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj if o.fields.length == 1 => + val keyValue = o.fields(0) + namesMap.get(keyValue._1) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, keyValue._2).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) + } + case _ => Lexer.error("expected single field object", trace) + } } + } else { + new JsonDecoder[A] { + private val spans = names.map(JsonError.ObjectAccess(_)) - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case o: Json.Obj if o.fields.length == 1 => - val keyValue = o.fields(0) - namesMap.get(keyValue._1) match { - case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, keyValue._2).asInstanceOf[A] - case _ => Lexer.error("invalid disambiguator", trace) - } - case _ => Lexer.error("expected single field object", trace) + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + Lexer.char(trace, in, '{') + if (Lexer.firstField(trace, in)) { + val idx = Lexer.field128(trace, in, matrix1, matrix2) + if (idx >= 0) { + val a = tcs(idx).unsafeDecode(spans(idx) :: trace, in).asInstanceOf[A] + Lexer.char(trace, in, '}') + a + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.error("expected non-empty object", trace) } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj if o.fields.length == 1 => + val keyValue = o.fields(0) + namesMap.get(keyValue._1) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, keyValue._2).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) + } + case _ => Lexer.error("expected single field object", trace) + } + } } } else { - new JsonDecoder[A] { - private val hintfield = discrim.get - private val hintmatrix = new StringMatrix(Array(hintfield)) - private val spans = names.map(JsonError.Message(_)) - - def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - val in_ = RecordingReader(in) - Lexer.char(trace, in_, '{') - if (Lexer.firstField(trace, in_)) { - while ({ - if (Lexer.field(trace, in_, hintmatrix) >= 0) { - val idx = Lexer.enumeration(trace, in_, matrix) - if (idx >= 0) { - in_.rewind() - return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] - } else Lexer.error("invalid disambiguator", trace) - } else Lexer.skipValue(trace, in_) - Lexer.nextField(trace, in_) - }) () + if (names.length <= 64) { + new JsonDecoder[A] { + private val hintfield = discrim.get + private val hintmatrix = new StringMatrix(Array(hintfield)) + private val spans = names.map(JsonError.Message(_)) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val in_ = RecordingReader(in) + Lexer.char(trace, in_, '{') + if (Lexer.firstField(trace, in_)) { + while ({ + if (Lexer.field(trace, in_, hintmatrix) >= 0) { + val idx = Lexer.enumeration(trace, in_, matrix1) + if (idx >= 0) { + in_.rewind() + return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.skipValue(trace, in_) + Lexer.nextField(trace, in_) + }) () + } + Lexer.error(s"missing hint '$hintfield'", trace) } - Lexer.error(s"missing hint '$hintfield'", trace) - } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case o: Json.Obj => - o.fields.collectFirst { case kv if kv._1 == hintfield && kv._2.isInstanceOf[Json.Str] => - kv._2.asInstanceOf[Json.Str].value - } match { - case Some(name) => - namesMap.get(name) match { - case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] - case _ => Lexer.error("invalid disambiguator", trace) - } - case _ => Lexer.error(s"missing hint '$hintfield'", trace) - } - case _ => Lexer.error("expected object", trace) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj => + o.fields.collectFirst { case kv if kv._1 == hintfield && kv._2.isInstanceOf[Json.Str] => + kv._2.asInstanceOf[Json.Str].value + } match { + case Some(name) => + namesMap.get(name) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) + } + case _ => Lexer.error(s"missing hint '$hintfield'", trace) + } + case _ => Lexer.error("expected object", trace) + } + } + } else { + new JsonDecoder[A] { + private val hintfield = discrim.get + private val hintmatrix = new StringMatrix(Array(hintfield)) + private val spans = names.map(JsonError.Message(_)) + + def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + val in_ = RecordingReader(in) + Lexer.char(trace, in_, '{') + if (Lexer.firstField(trace, in_)) { + while ({ + if (Lexer.field(trace, in_, hintmatrix) >= 0) { + val idx = Lexer.enumeration128(trace, in_, matrix1, matrix2) + if (idx >= 0) { + in_.rewind() + return tcs(idx).unsafeDecode(spans(idx) :: trace, in_).asInstanceOf[A] + } else Lexer.error("invalid disambiguator", trace) + } else Lexer.skipValue(trace, in_) + Lexer.nextField(trace, in_) + }) () + } + Lexer.error(s"missing hint '$hintfield'", trace) } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj => + o.fields.collectFirst { case kv if kv._1 == hintfield && kv._2.isInstanceOf[Json.Str] => + kv._2.asInstanceOf[Json.Str].value + } match { + case Some(name) => + namesMap.get(name) match { + case Some(idx) => tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, json).asInstanceOf[A] + case _ => Lexer.error("invalid disambiguator", trace) + } + case _ => Lexer.error(s"missing hint '$hintfield'", trace) + } + case _ => Lexer.error("expected object", trace) + } + } } } } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 9e7cb7952..886bad3b1 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -93,6 +93,32 @@ object Lexer { matrix.first(matrix.exact(bs, i)) } + def enumeration128(trace: List[JsonError], in: OneCharReader, matrix1: StringMatrix, matrix2: StringMatrix): Int = { + var c = in.nextNonWhitespace() + if (c != '"') error("'\"'", c, trace) + var bs1 = matrix1.initial + var bs2 = matrix2.initial + var i = 0 + while ({ + c = in.readChar() + c != '"' + }) { + if (c == '\\') c = nextEscaped(trace, in) + else if (c < ' ') error("invalid control in string", trace) + bs1 = matrix1.update(bs1, i, c) + bs2 = matrix2.update(bs2, i, c) + i += 1 + } + matrix1.first(matrix1.exact(bs1, i)) & (matrix2.first(matrix2.exact(bs2, i)) | 64) + } + + @inline def field128(trace: List[JsonError], in: OneCharReader, matrix1: StringMatrix, matrix2: StringMatrix): Int = { + val f = enumeration128(trace, in, matrix1, matrix2) + val c = in.nextNonWhitespace() + if (c == ':') return f + error("':'", c, trace) + } + @noinline def skipValue(trace: List[JsonError], in: RetractReader): Unit = (in.nextNonWhitespace(): @switch) match { case 'n' | 't' => skipFixedChars(in, 3) diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index e3f72cee3..b8f4c77f3 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -399,6 +399,11 @@ object DecoderSpec extends ZIOSpecDefault { assert("""{"hint":"Child2"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && assert("""{"child1":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) }, + test("sum more than 64 cases") { + import example100cases._ + + assert(""""B100"""".fromJson[A])(isRight(equalTo(A.B100))) + }, test("sum with duplicated case names") { for { error <- ZIO.attempt { @@ -1253,4 +1258,116 @@ object DecoderSpec extends ZIOSpecDefault { object Message { implicit val decoder: JsonDecoder[Message] = DeriveJsonDecoder.gen[Message] } + + object example100cases { + implicit val config: JsonCodecConfiguration = + JsonCodecConfiguration(enumValuesAsStrings = true) + + sealed trait A extends Product with Serializable + + object A { + case object B1 extends A + case object B2 extends A + case object B3 extends A + case object B4 extends A + case object B5 extends A + case object B6 extends A + case object B7 extends A + case object B8 extends A + case object B9 extends A + case object B10 extends A + case object B11 extends A + case object B12 extends A + case object B13 extends A + case object B14 extends A + case object B15 extends A + case object B16 extends A + case object B17 extends A + case object B18 extends A + case object B19 extends A + case object B20 extends A + case object B21 extends A + case object B22 extends A + case object B23 extends A + case object B24 extends A + case object B25 extends A + case object B26 extends A + case object B27 extends A + case object B28 extends A + case object B29 extends A + case object B30 extends A + case object B31 extends A + case object B32 extends A + case object B33 extends A + case object B34 extends A + case object B35 extends A + case object B36 extends A + case object B37 extends A + case object B38 extends A + case object B39 extends A + case object B40 extends A + case object B41 extends A + case object B42 extends A + case object B43 extends A + case object B44 extends A + case object B45 extends A + case object B46 extends A + case object B47 extends A + case object B48 extends A + case object B49 extends A + case object B50 extends A + case object B51 extends A + case object B52 extends A + case object B53 extends A + case object B54 extends A + case object B55 extends A + case object B56 extends A + case object B57 extends A + case object B58 extends A + case object B59 extends A + case object B60 extends A + case object B61 extends A + case object B62 extends A + case object B63 extends A + case object B64 extends A + case object B65 extends A + case object B66 extends A + case object B67 extends A + case object B68 extends A + case object B69 extends A + case object B70 extends A + case object B71 extends A + case object B72 extends A + case object B73 extends A + case object B74 extends A + case object B75 extends A + case object B76 extends A + case object B77 extends A + case object B78 extends A + case object B79 extends A + case object B80 extends A + case object B81 extends A + case object B82 extends A + case object B83 extends A + case object B84 extends A + case object B85 extends A + case object B86 extends A + case object B87 extends A + case object B88 extends A + case object B89 extends A + case object B90 extends A + case object B91 extends A + case object B92 extends A + case object B93 extends A + case object B94 extends A + case object B95 extends A + case object B96 extends A + case object B97 extends A + case object B98 extends A + case object B99 extends A + case object B100 extends A + + implicit val codec: JsonCodec[A] = DeriveJsonCodec.gen[A] + } + } } From 101e331f47e3392fdb3132fdee4cafd771ca3b77 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 10 Apr 2025 09:22:08 +0200 Subject: [PATCH 217/311] Add support up to 128 fields in product types (#1375) --- .../src/main/scala-2.x/zio/json/macros.scala | 354 ++++++++++++------ .../src/main/scala-3/zio/json/macros.scala | 326 +++++++++++----- .../main/scala/zio/json/internal/lexer.scala | 37 +- .../src/test/scala/zio/json/DecoderSpec.scala | 116 +++++- 4 files changed, 602 insertions(+), 231 deletions(-) diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 184f32933..05644d77e 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -5,8 +5,8 @@ import zio.Chunk import zio.json.JsonDecoder.JsonError import zio.json.ast.Json import zio.json.internal.{ FieldEncoder, Lexer, RecordingReader, RetractReader, StringMatrix, Write } - import scala.annotation._ +import scala.collection.mutable.ArrayBuffer import scala.language.experimental.macros /** @@ -226,9 +226,10 @@ object DeriveJsonDecoder { }.isDefined || !config.allowExtraFields if (ctx.parameters.isEmpty) new CaseObjectDecoder(ctx, no_extra) else { + var splitIndex = -1 val (names, aliases): (Array[String], Array[(String, Int)]) = { val names = new Array[String](ctx.parameters.size) - val aliasesBuilder = Array.newBuilder[(String, Int)] + val aliasesBuilder = new ArrayBuffer[(String, Int)] ctx.parameters.foreach { var idx = 0 p => @@ -238,8 +239,9 @@ object DeriveJsonDecoder { case _ => Seq.empty } idx += 1 + if (splitIndex < 0 && idx + aliasesBuilder.length > 64) splitIndex = idx - 1 } - val aliases = aliasesBuilder.result() + val aliases = aliasesBuilder.toArray val allFieldNames = names ++ aliases.map(_._1) if (allFieldNames.length != allFieldNames.distinct.length) { val aliasNames = aliases.map(_._1) @@ -252,129 +254,263 @@ object DeriveJsonDecoder { } (names, aliases) } - new CollectionJsonDecoder[A] { - private[this] val len = names.length - private[this] val matrix = new StringMatrix(names, aliases) - private[this] val spans = names.map(JsonError.ObjectAccess) - private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray - private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] - private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap - private[this] val explicitEmptyCollections = - ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => - a.decoding - }.getOrElse(config.explicitEmptyCollections.decoding) - private[this] val missingValueDecoder = - if (explicitEmptyCollections) { - lazy val missingValueDecoders = tcs.map { d => - if (allowMissingValueDecoder(d)) d - else null + if (splitIndex < 0) { + new CollectionJsonDecoder[A] { + private[this] val len = names.length + private[this] val matrix = new StringMatrix(names, aliases) + private[this] val spans = names.map(JsonError.ObjectAccess) + private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray + private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] + private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap + private[this] val explicitEmptyCollections = + ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.decoding + }.getOrElse(config.explicitEmptyCollections.decoding) + private[this] val missingValueDecoder = + if (explicitEmptyCollections) { + lazy val missingValueDecoders = tcs.map { d => + if (allowMissingValueDecoder(d)) d + else null + } + (idx: Int, trace: List[JsonError]) => { + val trace_ = spans(idx) :: trace + val decoder = missingValueDecoders(idx) + if (decoder eq null) Lexer.error("missing", trace_) + decoder.unsafeDecodeMissing(trace_) + } + } else { (idx: Int, trace: List[JsonError]) => + tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } - (idx: Int, trace: List[JsonError]) => { - val trace_ = spans(idx) :: trace - val decoder = missingValueDecoders(idx) - if (decoder eq null) Lexer.error("missing", trace_) - decoder.unsafeDecodeMissing(trace_) + + @tailrec + private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { + case _: OptionJsonDecoder[_] => true + case _: CollectionJsonDecoder[_] => !explicitEmptyCollections + case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) + case _ => true + } + + override def unsafeDecodeMissing(trace: List[JsonError]): A = { + val ps = new Array[Any](len) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 } - } else { (idx: Int, trace: List[JsonError]) => - tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + ctx.rawConstruct(new ArraySeq(ps)) } - @tailrec - private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { - case _: OptionJsonDecoder[_] => true - case _: CollectionJsonDecoder[_] => !explicitEmptyCollections - case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) - case _ => true - } + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + Lexer.char(trace, in, '{') - override def unsafeDecodeMissing(trace: List[JsonError]): A = { - val ps = new Array[Any](len) - var idx = 0 - while (idx < len) { - if (ps(idx) == null) { - val default = defaults(idx) - ps(idx) = - if (default ne null) default() - else missingValueDecoder(idx, trace) + // TODO it would be more efficient to have a solution that didn't box + // primitives, but Magnolia does not ealiasesxpose an API for that. Adding + // such a feature to Magnolia is the only way to avoid this, e.g. a + // ctx.createMutableCons that specialises on the types (with some way + // of noting that things have been initialised), which can be called + // to instantiate the case class. Would also require JsonDecoder to be + // specialised. + val ps = new Array[Any](len) + if (Lexer.firstField(trace, in)) { + do { + val idx = Lexer.field(trace, in, matrix) + if (idx >= 0) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if ( + (default eq null) || in.nextNonWhitespace() != 'n' && { + in.retract() + true + } + ) tcs(idx).unsafeDecode(spans(idx) :: trace, in) + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() + else Lexer.error("expected 'null'", spans(idx) :: trace) + } else Lexer.error("duplicate", trace) + } else if (no_extra) Lexer.error("invalid extra field", trace) + else Lexer.skipValue(trace, in) + } while (Lexer.nextField(trace, in)) } - idx += 1 + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 + } + ctx.rawConstruct(new ArraySeq(ps)) } - ctx.rawConstruct(new ArraySeq(ps)) + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj => + val ps = new Array[Any](len) + o.fields.foreach { kv => + namesMap.get(kv._1) match { + case Some(idx) => + if (ps(idx) != null) Lexer.error("duplicate", trace) + val default = defaults(idx) + ps(idx) = + if ((default ne null) && (kv._2 eq Json.Null)) default() + else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) + case _ => + if (no_extra) Lexer.error("invalid extra field", trace) + } + } + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 + } + ctx.rawConstruct(new ArraySeq(ps)) + case _ => Lexer.error("expected object", trace) + } } + } else { + val (names1, names2) = names.splitAt(splitIndex) + val aliases1 = aliases.filter(kv => kv._2 <= splitIndex) + val aliases2 = aliases.collect { + case (k, v) if v > splitIndex => + (k, v - splitIndex) + } + new CollectionJsonDecoder[A] { + private[this] val len = names.length + private[this] val matrix1 = new StringMatrix(names1, aliases1) + private[this] val matrix2 = new StringMatrix(names2, aliases2) + private[this] val spans = names.map(JsonError.ObjectAccess) + private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray + private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] + private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap + private[this] val explicitEmptyCollections = + ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.decoding + }.getOrElse(config.explicitEmptyCollections.decoding) + private[this] val missingValueDecoder = + if (explicitEmptyCollections) { + lazy val missingValueDecoders = tcs.map { d => + if (allowMissingValueDecoder(d)) d + else null + } + (idx: Int, trace: List[JsonError]) => { + val trace_ = spans(idx) :: trace + val decoder = missingValueDecoders(idx) + if (decoder eq null) Lexer.error("missing", trace_) + decoder.unsafeDecodeMissing(trace_) + } + } else { (idx: Int, trace: List[JsonError]) => + tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + } - override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - Lexer.char(trace, in, '{') - - // TODO it would be more efficient to have a solution that didn't box - // primitives, but Magnolia does not ealiasesxpose an API for that. Adding - // such a feature to Magnolia is the only way to avoid this, e.g. a - // ctx.createMutableCons that specialises on the types (with some way - // of noting that things have been initialised), which can be called - // to instantiate the case class. Would also require JsonDecoder to be - // specialised. - val ps = new Array[Any](len) - if (Lexer.firstField(trace, in)) { - do { - val idx = Lexer.field(trace, in, matrix) - if (idx >= 0) { - if (ps(idx) == null) { - val default = defaults(idx) - ps(idx) = - if ( - (default eq null) || in.nextNonWhitespace() != 'n' && { - in.retract() - true - } - ) tcs(idx).unsafeDecode(spans(idx) :: trace, in) - else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() - else Lexer.error("expected 'null'", spans(idx) :: trace) - } else Lexer.error("duplicate", trace) - } else if (no_extra) Lexer.error("invalid extra field", trace) - else Lexer.skipValue(trace, in) - } while (Lexer.nextField(trace, in)) + @tailrec + private[this] def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { + case _: OptionJsonDecoder[_] => true + case _: CollectionJsonDecoder[_] => !explicitEmptyCollections + case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) + case _ => true } - var idx = 0 - while (idx < len) { - if (ps(idx) == null) { - val default = defaults(idx) - ps(idx) = - if (default ne null) default() - else missingValueDecoder(idx, trace) + + override def unsafeDecodeMissing(trace: List[JsonError]): A = { + val ps = new Array[Any](len) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 } - idx += 1 + ctx.rawConstruct(new ArraySeq(ps)) } - ctx.rawConstruct(new ArraySeq(ps)) - } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case o: Json.Obj => - val ps = new Array[Any](len) - o.fields.foreach { kv => - namesMap.get(kv._1) match { - case Some(idx) => - if (ps(idx) != null) Lexer.error("duplicate", trace) + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + Lexer.char(trace, in, '{') + + // TODO it would be more efficient to have a solution that didn't box + // primitives, but Magnolia does not ealiasesxpose an API for that. Adding + // such a feature to Magnolia is the only way to avoid this, e.g. a + // ctx.createMutableCons that specialises on the types (with some way + // of noting that things have been initialised), which can be called + // to instantiate the case class. Would also require JsonDecoder to be + // specialised. + val ps = new Array[Any](len) + if (Lexer.firstField(trace, in)) { + do { + val idx = Lexer.field128(trace, in, matrix1, matrix2) + if (idx >= 0) { + if (ps(idx) == null) { val default = defaults(idx) ps(idx) = - if ((default ne null) && (kv._2 eq Json.Null)) default() - else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) - case _ => - if (no_extra) Lexer.error("invalid extra field", trace) - } - } - var idx = 0 - while (idx < len) { - if (ps(idx) == null) { - val default = defaults(idx) - ps(idx) = - if (default ne null) default() - else missingValueDecoder(idx, trace) - } - idx += 1 + if ( + (default eq null) || in.nextNonWhitespace() != 'n' && { + in.retract() + true + } + ) tcs(idx).unsafeDecode(spans(idx) :: trace, in) + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() + else Lexer.error("expected 'null'", spans(idx) :: trace) + } else Lexer.error("duplicate", trace) + } else if (no_extra) Lexer.error("invalid extra field", trace) + else Lexer.skipValue(trace, in) + } while (Lexer.nextField(trace, in)) + } + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) } - ctx.rawConstruct(new ArraySeq(ps)) - case _ => Lexer.error("expected object", trace) + idx += 1 + } + ctx.rawConstruct(new ArraySeq(ps)) } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj => + val ps = new Array[Any](len) + o.fields.foreach { kv => + namesMap.get(kv._1) match { + case Some(idx) => + if (ps(idx) != null) Lexer.error("duplicate", trace) + val default = defaults(idx) + ps(idx) = + if ((default ne null) && (kv._2 eq Json.Null)) default() + else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) + case _ => + if (no_extra) Lexer.error("invalid extra field", trace) + } + } + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 + } + ctx.rawConstruct(new ArraySeq(ps)) + case _ => Lexer.error("expected object", trace) + } + } } } } diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index f80555d45..77f6e72f8 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -236,6 +236,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv }.isDefined || !config.allowExtraFields if (ctx.params.isEmpty) new CaseObjectDecoder(ctx, no_extra) else { + var splitIndex = -1 val (names, aliases): (Array[String], Array[(String, Int)]) = { val names = new Array[String](ctx.params.size) val aliasesBuilder = Array.newBuilder[(String, Int)] @@ -248,6 +249,7 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv case _ => Seq.empty } idx += 1 + if (splitIndex < 0 && idx + aliasesBuilder.length > 64) splitIndex = idx - 1 } val aliases = aliasesBuilder.result() val allFieldNames = names ++ aliases.map(_._1) @@ -262,120 +264,244 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv } (names, aliases) } - new CollectionJsonDecoder[A] { - private val len = names.length - private val matrix = new StringMatrix(names, aliases) - private val spans = names.map(JsonError.ObjectAccess(_)) - private val defaults = IArray.genericWrapArray(ctx.params.map(_.evaluateDefault.orNull)).toArray - private lazy val tcs = - IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] - private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap - private val explicitEmptyCollections = - ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => - a.decoding - }.getOrElse(config.explicitEmptyCollections.decoding) - private val missingValueDecoder = - if (explicitEmptyCollections) { - lazy val missingValueDecoders = tcs.map { d => - if (allowMissingValueDecoder(d)) d - else null + if (splitIndex < 0) { + new CollectionJsonDecoder[A] { + private val len = names.length + private val matrix = new StringMatrix(names, aliases) + private val spans = names.map(JsonError.ObjectAccess(_)) + private val defaults = IArray.genericWrapArray(ctx.params.map(_.evaluateDefault.orNull)).toArray + private lazy val tcs = + IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] + private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap + private val explicitEmptyCollections = + ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.decoding + }.getOrElse(config.explicitEmptyCollections.decoding) + private val missingValueDecoder = + if (explicitEmptyCollections) { + lazy val missingValueDecoders = tcs.map { d => + if (allowMissingValueDecoder(d)) d + else null + } + (idx: Int, trace: List[JsonError]) => { + val trace_ = spans(idx) :: trace + val decoder = missingValueDecoders(idx) + if (decoder eq null) Lexer.error("missing", trace_) + decoder.unsafeDecodeMissing(trace_) + } + } else { + (idx: Int, trace: List[JsonError]) => tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) } - (idx: Int, trace: List[JsonError]) => { - val trace_ = spans(idx) :: trace - val decoder = missingValueDecoders(idx) - if (decoder eq null) Lexer.error("missing", trace_) - decoder.unsafeDecodeMissing(trace_) + + @tailrec + private def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { + case _: OptionJsonDecoder[_] => true + case _: CollectionJsonDecoder[_] => !explicitEmptyCollections + case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) + case _ => true + } + + override def unsafeDecodeMissing(trace: List[JsonError]): A = { + val ps = new Array[Any](len) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 } - } else { - (idx: Int, trace: List[JsonError]) => tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + ctx.rawConstruct(ps) } - @tailrec - private def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { - case _: OptionJsonDecoder[_] => true - case _: CollectionJsonDecoder[_] => !explicitEmptyCollections - case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) - case _ => true + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + Lexer.char(trace, in, '{') + val ps = new Array[Any](len) + if (Lexer.firstField(trace, in)) + while({ + val idx = Lexer.field(trace, in, matrix) + if (idx >= 0) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = if ((default eq null) || in.nextNonWhitespace() != 'n' && { + in.retract() + true + }) tcs(idx).unsafeDecode(spans(idx) :: trace, in) + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() + else Lexer.error("expected 'null'", spans(idx) :: trace) + } else Lexer.error("duplicate", trace) + } else if (no_extra) Lexer.error("invalid extra field", trace) + else Lexer.skipValue(trace, in) + Lexer.nextField(trace, in) + }) () + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 + } + ctx.rawConstruct(ps) + } + + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj => + val ps = new Array[Any](len) + o.fields.foreach { kv => + namesMap.get(kv._1) match { + case Some(idx) => + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if ((default ne null) && (kv._2 eq Json.Null)) default() + else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) + } else Lexer.error("duplicate", trace) + case _ => + if (no_extra) Lexer.error("invalid extra field", trace) + } + } + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 + } + ctx.rawConstruct(ps) + case _ => Lexer.error("expected object", trace) + } + } + } else { + val (names1, names2) = names.splitAt(splitIndex) + val aliases1 = aliases.filter(kv => kv._2 <= splitIndex) + val aliases2 = aliases.collect { case (k, v) if v > splitIndex => + (k, v - splitIndex) } + new CollectionJsonDecoder[A] { + private val len = names.length + private val matrix1 = new StringMatrix(names1, aliases1) + private val matrix2 = new StringMatrix(names2, aliases2) + private val spans = names.map(JsonError.ObjectAccess(_)) + private val defaults = IArray.genericWrapArray(ctx.params.map(_.evaluateDefault.orNull)).toArray + private lazy val tcs = + IArray.genericWrapArray(ctx.params.map(_.typeclass)).toArray.asInstanceOf[Array[JsonDecoder[Any]]] + private lazy val namesMap = (names.zipWithIndex ++ aliases).toMap + private val explicitEmptyCollections = + ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => + a.decoding + }.getOrElse(config.explicitEmptyCollections.decoding) + private val missingValueDecoder = + if (explicitEmptyCollections) { + lazy val missingValueDecoders = tcs.map { d => + if (allowMissingValueDecoder(d)) d + else null + } + (idx: Int, trace: List[JsonError]) => { + val trace_ = spans(idx) :: trace + val decoder = missingValueDecoders(idx) + if (decoder eq null) Lexer.error("missing", trace_) + decoder.unsafeDecodeMissing(trace_) + } + } else { + (idx: Int, trace: List[JsonError]) => tcs(idx).unsafeDecodeMissing(spans(idx) :: trace) + } - override def unsafeDecodeMissing(trace: List[JsonError]): A = { - val ps = new Array[Any](len) - var idx = 0 - while (idx < len) { - if (ps(idx) == null) { - val default = defaults(idx) - ps(idx) = - if (default ne null) default() - else missingValueDecoder(idx, trace) + @tailrec + private def allowMissingValueDecoder(d: JsonDecoder[_]): Boolean = d match { + case _: OptionJsonDecoder[_] => true + case _: CollectionJsonDecoder[_] => !explicitEmptyCollections + case d: MappedJsonDecoder[_] => allowMissingValueDecoder(d.underlying) + case _ => true + } + + override def unsafeDecodeMissing(trace: List[JsonError]): A = { + val ps = new Array[Any](len) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 } - idx += 1 + ctx.rawConstruct(ps) } - ctx.rawConstruct(ps) - } - override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { - Lexer.char(trace, in, '{') - val ps = new Array[Any](len) - if (Lexer.firstField(trace, in)) - while({ - val idx = Lexer.field(trace, in, matrix) - if (idx >= 0) { - if (ps(idx) == null) { - val default = defaults(idx) - ps(idx) = if ((default eq null) || in.nextNonWhitespace() != 'n' && { - in.retract() - true - }) tcs(idx).unsafeDecode(spans(idx) :: trace, in) - else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() - else Lexer.error("expected 'null'", spans(idx) :: trace) - } else Lexer.error("duplicate", trace) - } else if (no_extra) Lexer.error("invalid extra field", trace) - else Lexer.skipValue(trace, in) - Lexer.nextField(trace, in) - }) () - var idx = 0 - while (idx < len) { - if (ps(idx) == null) { - val default = defaults(idx) - ps(idx) = - if (default ne null) default() - else missingValueDecoder(idx, trace) + override def unsafeDecode(trace: List[JsonError], in: RetractReader): A = { + Lexer.char(trace, in, '{') + val ps = new Array[Any](len) + if (Lexer.firstField(trace, in)) + while({ + val idx = Lexer.field128(trace, in, matrix1, matrix2) + if (idx >= 0) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = if ((default eq null) || in.nextNonWhitespace() != 'n' && { + in.retract() + true + }) tcs(idx).unsafeDecode(spans(idx) :: trace, in) + else if (in.readChar() == 'u' && in.readChar() == 'l' && in.readChar() == 'l') default() + else Lexer.error("expected 'null'", spans(idx) :: trace) + } else Lexer.error("duplicate", trace) + } else if (no_extra) Lexer.error("invalid extra field", trace) + else Lexer.skipValue(trace, in) + Lexer.nextField(trace, in) + }) () + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 } - idx += 1 + ctx.rawConstruct(ps) } - ctx.rawConstruct(ps) - } - override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = - json match { - case o: Json.Obj => - val ps = new Array[Any](len) - o.fields.foreach { kv => - namesMap.get(kv._1) match { - case Some(idx) => - if (ps(idx) == null) { - val default = defaults(idx) - ps(idx) = - if ((default ne null) && (kv._2 eq Json.Null)) default() - else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) - } else Lexer.error("duplicate", trace) - case _ => - if (no_extra) Lexer.error("invalid extra field", trace) + override final def unsafeFromJsonAST(trace: List[JsonError], json: Json): A = + json match { + case o: Json.Obj => + val ps = new Array[Any](len) + o.fields.foreach { kv => + namesMap.get(kv._1) match { + case Some(idx) => + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if ((default ne null) && (kv._2 eq Json.Null)) default() + else tcs(idx).unsafeFromJsonAST(spans(idx) :: trace, kv._2) + } else Lexer.error("duplicate", trace) + case _ => + if (no_extra) Lexer.error("invalid extra field", trace) + } } - } - var idx = 0 - while (idx < len) { - if (ps(idx) == null) { - val default = defaults(idx) - ps(idx) = - if (default ne null) default() - else missingValueDecoder(idx, trace) + var idx = 0 + while (idx < len) { + if (ps(idx) == null) { + val default = defaults(idx) + ps(idx) = + if (default ne null) default() + else missingValueDecoder(idx, trace) + } + idx += 1 } - idx += 1 - } - ctx.rawConstruct(ps) - case _ => Lexer.error("expected object", trace) - } + ctx.rawConstruct(ps) + case _ => Lexer.error("expected object", trace) + } + } } } } diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 886bad3b1..44c976db5 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -109,7 +109,9 @@ object Lexer { bs2 = matrix2.update(bs2, i, c) i += 1 } - matrix1.first(matrix1.exact(bs1, i)) & (matrix2.first(matrix2.exact(bs2, i)) | 64) + var idx = matrix1.first(matrix1.exact(bs1, i)) + if (idx < 0) idx = matrix2.first(matrix2.exact(bs2, i)) + matrix1.namesLen + idx } @inline def field128(trace: List[JsonError], in: OneCharReader, matrix1: StringMatrix, matrix2: StringMatrix): Int = { @@ -1856,27 +1858,22 @@ object Lexer { // A data structure encoding a simple algorithm for Trie pruning: Given a list // of strings, and a sequence of incoming characters, find the strings that // match, by manually maintaining a bitset. Empty strings are not allowed. -final class StringMatrix(xs: Array[String], aliases: Array[(String, Int)] = Array.empty) { - require(xs.nonEmpty) - - private[this] val width: Int = xs.length + aliases.length - - require(width <= 64) - - val initial: Long = -1L >>> (64 - width) - +final class StringMatrix(names: Array[String], aliases: Array[(String, Int)] = Array.empty) { + val namesLen: Int = names.length + private[this] val width: Int = namesLen + aliases.length + val initial: Long = -1L >>> (64 - width) private[this] val lengths: Array[Int] = { + require(namesLen > 0 && width <= 64) val ls = new Array[Int](width) - val xsLen = xs.length var string = 0 - while (string < xsLen) { - val l = xs(string).length + while (string < namesLen) { + val l = names(string).length if (l == 0) require(false) ls(string) = l string += 1 } while (string < ls.length) { - val l = aliases(string - xsLen)._1.length + val l = aliases(string - namesLen)._1.length if (l == 0) require(false) ls(string) = l string += 1 @@ -1887,12 +1884,11 @@ final class StringMatrix(xs: Array[String], aliases: Array[(String, Int)] = Arra private[this] val matrix: Array[Char] = { val w = width val m = new Array[Char](height * w) - val xsLen = xs.length var string = 0 while (string < w) { val s = - if (string < xsLen) xs(string) - else aliases(string - xsLen)._1 + if (string < namesLen) names(string) + else aliases(string - namesLen)._1 val len = s.length var char, base = 0 while (char < len) { @@ -1906,15 +1902,14 @@ final class StringMatrix(xs: Array[String], aliases: Array[(String, Int)] = Arra } private[this] val resolvers: Array[Byte] = { val rs = new Array[Byte](width) - val xsLen = xs.length var string = 0 - while (string < xsLen) { + while (string < namesLen) { rs(string) = string.toByte string += 1 } while (string < rs.length) { - val x = aliases(string - xsLen)._2 - if (x < 0 || x > xsLen) require(false) + val x = aliases(string - namesLen)._2 + if (x < 0 || x > namesLen) require(false) rs(string) = x.toByte string += 1 } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index b8f4c77f3..617b89ab2 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -399,7 +399,7 @@ object DecoderSpec extends ZIOSpecDefault { assert("""{"hint":"Child2"}""".fromJson[Parent])(isLeft(equalTo("(invalid disambiguator)"))) && assert("""{"child1":{}}""".fromJson[Parent])(isLeft(equalTo("(missing hint 'hint')"))) }, - test("sum more than 64 cases") { + test("sum with more than 64 cases") { import example100cases._ assert(""""B100"""".fromJson[A])(isRight(equalTo(A.B100))) @@ -923,6 +923,13 @@ object DecoderSpec extends ZIOSpecDefault { assert(Json.Obj().as[DefaultString])(isRight(equalTo(DefaultString("")))) && assert(Json.Obj("s" -> Json.Null).as[DefaultString])(isRight(equalTo(DefaultString("")))) }, + test("product with more than 64 fields") { + import example100fields._ + + assert("""{"f01":1,"F60":60,"f70":70,"f109":100}""".fromJson[A])( + isRight(equalTo(A(f01 = Some(1), f60 = Some(60), f70 = Some(70), f100 = Some(100)))) + ) + }, test("dynamic default value") { case class DefaultDynamic( randomNumber: Double = scala.math.random(), @@ -1370,4 +1377,111 @@ object DecoderSpec extends ZIOSpecDefault { implicit val codec: JsonCodec[A] = DeriveJsonCodec.gen[A] } } + + object example100fields { + case class A( + f01: Option[Int] = None, + f02: Option[Int] = None, + f03: Option[Int] = None, + f04: Option[Int] = None, + f05: Option[Int] = None, + f06: Option[Int] = None, + f07: Option[Int] = None, + f08: Option[Int] = None, + f09: Option[Int] = None, + f10: Option[Int] = None, + f11: Option[Int] = None, + f12: Option[Int] = None, + f13: Option[Int] = None, + f14: Option[Int] = None, + f15: Option[Int] = None, + f16: Option[Int] = None, + f17: Option[Int] = None, + f18: Option[Int] = None, + f19: Option[Int] = None, + f20: Option[Int] = None, + f21: Option[Int] = None, + f22: Option[Int] = None, + f23: Option[Int] = None, + f24: Option[Int] = None, + f25: Option[Int] = None, + f26: Option[Int] = None, + f27: Option[Int] = None, + f28: Option[Int] = None, + f29: Option[Int] = None, + f30: Option[Int] = None, + f31: Option[Int] = None, + f32: Option[Int] = None, + f33: Option[Int] = None, + f34: Option[Int] = None, + f35: Option[Int] = None, + f36: Option[Int] = None, + f37: Option[Int] = None, + f38: Option[Int] = None, + f39: Option[Int] = None, + f40: Option[Int] = None, + f41: Option[Int] = None, + f42: Option[Int] = None, + f43: Option[Int] = None, + f44: Option[Int] = None, + f45: Option[Int] = None, + f46: Option[Int] = None, + f47: Option[Int] = None, + f48: Option[Int] = None, + f49: Option[Int] = None, + f50: Option[Int] = None, + f51: Option[Int] = None, + f52: Option[Int] = None, + f53: Option[Int] = None, + f54: Option[Int] = None, + f55: Option[Int] = None, + f56: Option[Int] = None, + f57: Option[Int] = None, + f58: Option[Int] = None, + f59: Option[Int] = None, + @jsonAliases("f_60", "f-60", "F60", "_f60") f60: Option[Int] = None, + f61: Option[Int] = None, + f62: Option[Int] = None, + f63: Option[Int] = None, + f64: Option[Int] = None, + f65: Option[Int] = None, + f66: Option[Int] = None, + f67: Option[Int] = None, + f68: Option[Int] = None, + f69: Option[Int] = None, + f70: Option[Int] = None, + f71: Option[Int] = None, + f72: Option[Int] = None, + f73: Option[Int] = None, + f74: Option[Int] = None, + f75: Option[Int] = None, + f76: Option[Int] = None, + f77: Option[Int] = None, + f78: Option[Int] = None, + f79: Option[Int] = None, + f80: Option[Int] = None, + f81: Option[Int] = None, + f82: Option[Int] = None, + f83: Option[Int] = None, + f84: Option[Int] = None, + f85: Option[Int] = None, + f86: Option[Int] = None, + f87: Option[Int] = None, + f88: Option[Int] = None, + f89: Option[Int] = None, + f90: Option[Int] = None, + f91: Option[Int] = None, + f92: Option[Int] = None, + f93: Option[Int] = None, + f94: Option[Int] = None, + f95: Option[Int] = None, + f96: Option[Int] = None, + f97: Option[Int] = None, + f98: Option[Int] = None, + f99: Option[Int] = None, + @jsonAliases("f101", "f102", "f103", "f104", "f105", "f106", "f107", "f108", "f109") f100: Option[Int] = None + ) + + implicit val codec: JsonCodec[A] = DeriveJsonCodec.gen[A] + } } From 5da212ca34fd6c55ea7e9ece53a75272cef27b0e Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Thu, 10 Apr 2025 21:21:16 +0200 Subject: [PATCH 218/311] Fix unexpected "(duplicate)" error when decoding products with more than 64 fields (#1376) --- zio-json/shared/src/main/scala/zio/json/internal/lexer.scala | 5 ++++- zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index 44c976db5..ddf1e7416 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -110,7 +110,10 @@ object Lexer { i += 1 } var idx = matrix1.first(matrix1.exact(bs1, i)) - if (idx < 0) idx = matrix2.first(matrix2.exact(bs2, i)) + matrix1.namesLen + if (idx < 0) { + idx = matrix2.first(matrix2.exact(bs2, i)) + if (idx >= 0) idx += matrix1.namesLen + } idx } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 617b89ab2..934e70895 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -926,7 +926,7 @@ object DecoderSpec extends ZIOSpecDefault { test("product with more than 64 fields") { import example100fields._ - assert("""{"f01":1,"F60":60,"f70":70,"f109":100}""".fromJson[A])( + assert("""{"f01":1,"F60":60,"f70":70,"f109":100,"f129":129}""".fromJson[A])( isRight(equalTo(A(f01 = Some(1), f60 = Some(60), f70 = Some(70), f100 = Some(100)))) ) }, From 305fa86e2056822890a741bec8ed2282f171ecbd Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sun, 13 Apr 2025 09:38:46 +0200 Subject: [PATCH 219/311] Update Scala Native to 0.5.7 (#1377) --- project/BuildHelper.scala | 4 ++-- project/plugins.sbt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 5b3bd69ca..3747d2af9 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -285,12 +285,12 @@ object BuildHelper { def nativeSettings = Seq( nativeConfig ~= { cfg => - import scala.scalanative.build.{ GC, Mode } + import scala.scalanative.build.Mode val os = System.getProperty("os.name").toLowerCase // For some unknown reason, we can't run the test suites in debug mode on MacOS if (os.contains("mac")) cfg.withMode(Mode.releaseFast) - else cfg.withGC(GC.boehm) // See https://github.com/scala-native/scala-native/issues/4032 + else cfg }, scalacOptions += "-P:scalanative:genStaticForwardersForNonTopLevelObjects", Test / fork := false diff --git a/project/plugins.sbt b/project/plugins.sbt index f66d251ac..79adf72e6 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") From 473a7311c4113590e0f32622fc9b6ea01cdf010a Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 13 Apr 2025 09:40:13 +0200 Subject: [PATCH 220/311] Update circe-core, circe-generic, ... to 0.14.12 (#1364) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index cf18527cf..0ead78ca7 100644 --- a/build.sbt +++ b/build.sbt @@ -86,7 +86,7 @@ lazy val zioJsonRoot = project zioJsonGolden ) -val circeVersion = "0.14.10" +val circeVersion = "0.14.12" lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("zio-json")) From cb9c57316c1d6d2c9d82b13fd53a1a105c730d13 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 13 Apr 2025 09:40:29 +0200 Subject: [PATCH 221/311] Update jsoniter-scala-core, ... to 2.33.3 (#1366) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 0ead78ca7..60c053f4a 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.2" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.3" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.3" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 11338056423883ef2c1d71edc79073678e0629c5 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Tue, 15 Apr 2025 07:46:31 +0200 Subject: [PATCH 222/311] Update jsoniter-scala-core, ... to 2.34.0 (#1378) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 60c053f4a..80e58402e 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.33.3" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.33.3" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.34.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.34.0" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From c1c27c604116c71486e54174df4f78e22463d363 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 17 Apr 2025 07:18:18 +0200 Subject: [PATCH 223/311] Update jsoniter-scala-core, ... to 2.34.1 (#1379) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 80e58402e..82e731fd1 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.34.0" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.34.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.34.1" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.34.1" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From ad8f68a1c6e222b5162a5a4b6311bcdbdfac86e5 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sat, 19 Apr 2025 07:08:24 +0200 Subject: [PATCH 224/311] Update jsoniter-scala-core, ... to 2.35.0 (#1380) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 82e731fd1..18cc57929 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.34.1" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.34.1" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.35.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.35.0" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 3db17edf18b7bf1c6b5b9311fcb8da7634f787d3 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 22 Apr 2025 09:05:16 +0200 Subject: [PATCH 225/311] Update README.md --- README.md | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 9c7ba3da3..b5e5faa71 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The goal of this project is to create the best all-round JSON library for Scala: - **Performance** to handle more requests per second than the incumbents, i.e. reduced operational costs. - **Security** to mitigate against adversarial JSON payloads that threaten the capacity of the server. - **Fast Compilation** no shapeless, no type astronautics. -- **Future-Proof**, prepared for Scala 3 and next-generation Java. +- **Future-Proof**, prepared for Scala 3 and runs on JDK 11+ JVMs. - **Simple** small codebase, concise documentation that covers everything. - **Helpful errors** are readable by humans and machines. - **ZIO Integration** so nothing more is required. @@ -25,7 +25,7 @@ The goal of this project is to create the best all-round JSON library for Scala: In order to use this library, we need to add the following line in our `build.sbt` file: ```scala -libraryDependencies += "dev.zio" %% "zio-json" % "0.6.2" +libraryDependencies += "dev.zio" %% "zio-json" % "0.7.42" ``` ## Example @@ -58,8 +58,6 @@ object Banana { } ``` -_Note: If you’re using Scala 3 and your case class is defining default parameters, `-Yretain-trees` needs to be added to `scalacOptions`._ - Now we can parse JSON into our object ``` @@ -88,7 +86,7 @@ val res: String = And bad JSON will produce an error in `jq` syntax with an additional piece of contextual information (in parentheses) ``` -scala> """{"curvature": womp}""".fromJson[Banana] +scala> """{"curvature": true}""".fromJson[Banana] val res: Either[String, Banana] = Left(.curvature(expected a Double)) ``` @@ -121,35 +119,9 @@ val res: Either[String, Fruit] = Right(Apple(false)) Almost all of the standard library data types are supported as fields on the case class, and it is easy to add support if one is missing. -```scala -import zio.json._ - -sealed trait Fruit extends Product with Serializable -case class Banana(curvature: Double) extends Fruit -case class Apple(poison: Boolean) extends Fruit - -object Fruit { - implicit val decoder: JsonDecoder[Fruit] = - DeriveJsonDecoder.gen[Fruit] - - implicit val encoder: JsonEncoder[Fruit] = - DeriveJsonEncoder.gen[Fruit] -} - -val json1 = """{ "Banana":{ "curvature":0.5 }}""" -val json2 = """{ "Apple": { "poison": false }}""" -val malformedJson = """{ "Banana":{ "curvature": true }}""" - -json1.fromJson[Fruit] -json2.fromJson[Fruit] -malformedJson.fromJson[Fruit] - -List(Apple(false), Banana(0.4)).toJsonPretty -``` - # How -Extreme **performance** is achieved by decoding JSON directly from the input source into business objects (inspired by [plokhotnyuk](https://github.com/plokhotnyuk/jsoniter-scala)). Although not a requirement, the latest advances in [Java Loom](https://wiki.openjdk.java.net/display/loom/Main) can be used to support arbitrarily large payloads with near-zero overhead. +High **performance** is achieved by decoding JSON directly from the input source into business objects. See benchmark results of throughput and allocation rate for synthetic and real-world message samples in comparison with other JSON parsers [here](https://plokhotnyuk.github.io/jsoniter-scala/). Best in class **security** is achieved with an aggressive *early exit* strategy that avoids costly stack traces, even when parsing malformed numbers. Malicious (and badly formed) payloads are rejected before finishing reading. From 9b8a1435c480d6371a6c6b260519fe2bea397839 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 1 May 2025 08:17:10 +0200 Subject: [PATCH 226/311] Update scalafmt-core to 3.9.5 (#1385) --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index a16232c42..c08b05508 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.9.4" +version = "3.9.5" runner.dialect = scala213 maxColumn = 120 align.preset = most From 2633f67a0012f0d8d5cd6076c19a9d62c4538d8f Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 2 May 2025 09:22:06 +0200 Subject: [PATCH 227/311] Update sbt-scalajs, scalajs-compiler, ... to 1.19.0 (#1382) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 79adf72e6..8f846f15b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,7 @@ addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From dcad122dc1eff4508ca0d09ad13ff8152d4d038f Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 2 May 2025 09:22:22 +0200 Subject: [PATCH 228/311] Update circe-core, circe-generic, ... to 0.14.13 (#1381) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 18cc57929..60c63ab1f 100644 --- a/build.sbt +++ b/build.sbt @@ -86,7 +86,7 @@ lazy val zioJsonRoot = project zioJsonGolden ) -val circeVersion = "0.14.12" +val circeVersion = "0.14.13" lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("zio-json")) From 1d20b6eed61b667e3879e7d74eb89f7a2b552da9 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 2 May 2025 09:28:26 +0200 Subject: [PATCH 229/311] Update jsoniter-scala-core, ... to 2.35.2 (#1384) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 60c63ab1f..b06fdaee6 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.35.0" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.35.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.35.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.35.2" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 4e1268116714eab7206bb52f473f91e3319eb38f Mon Sep 17 00:00:00 2001 From: Paul Daniels Date: Sat, 10 May 2025 17:25:21 +0800 Subject: [PATCH 230/311] Optimize decoding ast variants of tuple and both (#1389) --- build.sbt | 16 +++++++++ .../src/main/scala/zio/json/JsonDecoder.scala | 6 ++++ .../src/test/scala/zio/json/DecoderSpec.scala | 34 +++++++++++++++++-- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index b06fdaee6..3ba71e74d 100644 --- a/build.sbt +++ b/build.sbt @@ -142,6 +142,9 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) val work = (1 to i) .map(p => s"val a$p = A$p.unsafeDecode(traces(${p - 1}) :: trace, in)") .mkString("\n Lexer.char(trace, in, ',')\n ") + val work2 = (1 to i) + .map(p => s"val a$p = A$p.unsafeFromJsonAST(traces(${p - 1}) :: trace, arr($p - 1))") + .mkString("\n ") val returns = (1 to i).map(p => s"a$p").mkString(", ") s"""implicit def tuple$i[$tparams](implicit $implicits): JsonDecoder[Tuple$i[$tparams]] = @@ -153,6 +156,18 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) | Lexer.char(trace, in, ']') | new Tuple$i($returns) | } + | override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Tuple$i[$tparams] = { + | json match { + | case a: Json.Arr => + | val arr = a.elements + | if (arr.length != $i) { + | Lexer.error("Expected array of size $i", trace) + | } + | $work2 + | new Tuple$i($returns) + | case _ => Lexer.error("Not an array", trace) + | } + | } | }""".stripMargin } IO.write( @@ -160,6 +175,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) s"""package zio.json | |import zio.json.internal._ + |import zio.json.ast._ | |private[json] trait GeneratedTupleDecoders { this: JsonDecoder.type => | ${decoders.mkString("\n\n ")} diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index b559c52c9..458961837 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -74,6 +74,12 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { val b = that.unsafeDecode(trace, rr) f(a, b) } + + override def unsafeFromJsonAST(trace: List[JsonError], json: Json): C = { + val a = self.unsafeFromJsonAST(trace, json) + val b = that.unsafeFromJsonAST(trace, json) + f(a, b) + } } /** diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 934e70895..550d286f6 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -235,12 +235,29 @@ object DecoderSpec extends ZIOSpecDefault { ) }, test("tuples") { - assert("""["a",3]""".fromJson[(String, Int)])(isRight(equalTo(("a", 3)))) - assert("""["a","b"]""".fromJson[(String, Int)])(isLeft(equalTo("[1](expected an Int)"))) + assert("""["a",3]""".fromJson[(String, Int)])(isRight(equalTo(("a", 3)))) && + assert("""["a","b"]""".fromJson[(String, Int)])(isLeft(equalTo("[1](expected an Int)"))) && assert("""[[0.1,0.2],[0.3,0.4],[-0.3,-]]""".fromJson[Seq[(Double, Double)]])( isLeft(equalTo("[2][1](expected a Double)")) ) }, + test("tuples - ast") { + val a = Json.Arr(Json.Str("a"), Json.Num(3)) + val b = Json.Arr(Json.Str("a"), Json.Str("b")) + val c = Json.Arr( + Json.Arr(Json.Num(0.1), Json.Num(0.2)), + Json.Arr(Json.Num(0.3), Json.Num(0.4)), + Json.Arr(Json.Num(-0.3), Json.Null) + ) + val d = Json.Arr(Json.Num(0.1)) + + assertTrue( + a.as[(String, Int)].is(_.right) == ("a" -> 3), + b.as[(String, String)].is(_.right) == ("a" -> "b"), + c.as[List[(Double, Double)]].is(_.left) == """[2][1](expected a Double)""", + d.as[(Double, Double)].is(_.left) == "(Expected array of size 2)" + ) + }, test("parameterless products") { import exampleproducts._ @@ -619,6 +636,19 @@ object DecoderSpec extends ZIOSpecDefault { json.fromJson[(Foo, Bar)] == Right((Foo(1), Bar("foo"))) ) }, + test("bothWith - ast") { + final case class Foo(a: Int) + final case class Bar(b: String) + + val fooDecoder: JsonDecoder[Foo] = DeriveJsonDecoder.gen + val barDecoder: JsonDecoder[Bar] = DeriveJsonDecoder.gen + implicit val fooAndBarDecoder: JsonDecoder[(Foo, Bar)] = fooDecoder.both(barDecoder) + + val json = Json.Obj("a" -> Json.Num(1), "b" -> Json.Str("foo")) + assertTrue( + json.as[(Foo, Bar)] == Right((Foo(1), Bar("foo"))) + ) + }, test("option custom codec") { val json = """{"keyStatus": "certified"}""" final case class Foo(v: String) From 6769ea2caf40136f498e5635c25b4a10f2c63f7c Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 10 May 2025 12:47:16 +0200 Subject: [PATCH 231/311] Clean of code generated by the build and macros (#1390) --- build.sbt | 11 ++-------- .../src/main/scala-2.x/zio/json/macros.scala | 22 +++++++++---------- .../src/main/scala-3/zio/json/macros.scala | 20 +++++++++-------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/build.sbt b/build.sbt index 3ba71e74d..734fe1a75 100644 --- a/build.sbt +++ b/build.sbt @@ -146,7 +146,6 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .map(p => s"val a$p = A$p.unsafeFromJsonAST(traces(${p - 1}) :: trace, arr($p - 1))") .mkString("\n ") val returns = (1 to i).map(p => s"a$p").mkString(", ") - s"""implicit def tuple$i[$tparams](implicit $implicits): JsonDecoder[Tuple$i[$tparams]] = | new JsonDecoder[Tuple$i[$tparams]] { | private[this] val traces: Array[JsonError] = (0 to ${i - 1}).map(JsonError.ArrayAccess(_)).toArray @@ -158,14 +157,10 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) | } | override def unsafeFromJsonAST(trace: List[JsonError], json: Json): Tuple$i[$tparams] = { | json match { - | case a: Json.Arr => - | val arr = a.elements - | if (arr.length != $i) { - | Lexer.error("Expected array of size $i", trace) - | } + | case Json.Arr(arr) if arr.length == $i => | $work2 | new Tuple$i($returns) - | case _ => Lexer.error("Not an array", trace) + | case _ => Lexer.error("Expected array of size $i", trace) | } | } | }""".stripMargin @@ -192,7 +187,6 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) val work = (1 to i) .map(p => s"A$p.unsafeEncode(t._$p, indent, out)") .mkString("\n if (indent.isEmpty) out.write(',') else out.write(\", \")\n ") - s"""implicit def tuple$i[$tparams](implicit $implicits): JsonEncoder[Tuple$i[$tparams]] = | new JsonEncoder[Tuple$i[$tparams]] { | def unsafeEncode(t: Tuple$i[$tparams], indent: Option[Int], out: internal.Write): Unit = { @@ -218,7 +212,6 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) val codecs = (1 to 22).map { i => val tparamDecls = (1 to i).map(p => s"A$p: JsonEncoder: JsonDecoder").mkString(", ") val tparams = (1 to i).map(p => s"A$p").mkString(", ") - s"""implicit def tuple$i[$tparamDecls]: JsonCodec[Tuple$i[$tparams]] = | JsonCodec(JsonEncoder.tuple$i, JsonDecoder.tuple$i)""".stripMargin } diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 05644d77e..913f7d261 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -244,13 +244,15 @@ object DeriveJsonDecoder { val aliases = aliasesBuilder.toArray val allFieldNames = names ++ aliases.map(_._1) if (allFieldNames.length != allFieldNames.distinct.length) { - val aliasNames = aliases.map(_._1) - val collisions = aliasNames - .filter(alias => names.contains(alias) || aliases.count(a => a._1 == alias) > 1) + val typeName = ctx.typeName.full + val collisions = aliases + .map(_._1) .distinct - val msg = s"Field names and aliases in case class ${ctx.typeName.full} must be distinct, " + - s"alias(es) ${collisions.mkString(",")} collide with a field or another alias" - throw new AssertionError(msg) + .filter(alias => names.contains(alias) || aliases.count(_._1 == alias) > 1) + .mkString(",") + throw new AssertionError( + s"Field names and aliases in case class $typeName must be distinct, alias(es) $collisions collide with a field or another alias" + ) } (names, aliases) } @@ -522,11 +524,9 @@ object DeriveJsonDecoder { p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeName.short)) }.toArray if (names.distinct.length != names.length) { - val collisions = names.groupBy(identity).collect { case (n, ns) if ns.lengthCompare(1) > 0 => n } - throw new AssertionError( - s"Case names in ADT ${ctx.typeName.full} must be distinct, " + - s"name(s) ${collisions.mkString(",")} are duplicated" - ) + val typeName = ctx.typeName.full + val collisions = names.groupBy(identity).collect { case (n, ns) if ns.lengthCompare(1) > 0 => n }.mkString(",") + throw new AssertionError(s"Case names in ADT $typeName must be distinct, name(s) $collisions are duplicated") } val (names1, names2) = names.splitAt(64) val matrix1 = new StringMatrix(names1) diff --git a/zio-json/shared/src/main/scala-3/zio/json/macros.scala b/zio-json/shared/src/main/scala-3/zio/json/macros.scala index 77f6e72f8..26e17fc9d 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/macros.scala @@ -254,13 +254,15 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv val aliases = aliasesBuilder.result() val allFieldNames = names ++ aliases.map(_._1) if (allFieldNames.length != allFieldNames.distinct.length) { - val aliasNames = aliases.map(_._1) - val collisions = aliasNames - .filter(alias => names.contains(alias) || aliases.count { case (a, _) => a == alias } > 1) + val typeName = ctx.typeInfo.full + val collisions = aliases + .map(_._1) .distinct - val msg = s"Field names and aliases in case class ${ctx.typeInfo.full} must be distinct, " + - s"alias(es) ${collisions.mkString(",")} collide with a field or another alias" - throw new AssertionError(msg) + .filter(alias => names.contains(alias) || aliases.count(_._1 == alias) > 1) + .mkString(",") + throw new AssertionError( + s"Field names and aliases in case class $typeName must be distinct, alias(es) $collisions collide with a field or another alias" + ) } (names, aliases) } @@ -513,9 +515,9 @@ sealed class JsonDecoderDerivation(config: JsonCodecConfiguration) extends Deriv p.annotations.collectFirst { case jsonHint(name) => name }.getOrElse(jsonHintFormat(p.typeInfo.short)) }).toArray if (names.distinct.length != names.length) { - val collisions = names.groupBy(identity).collect { case (n, ns) if ns.lengthCompare(1) > 0 => n } - throw new AssertionError(s"Case names in ADT ${ctx.typeInfo.full} must be distinct, " + - s"name(s) ${collisions.mkString(",")} are duplicated") + val typeName = ctx.typeInfo.full + val collisions = names.groupBy(identity).collect { case (n, ns) if ns.lengthCompare(1) > 0 => n }.mkString(",") + throw new AssertionError(s"Case names in ADT $typeName must be distinct, name(s) $collisions are duplicated") } val (names1, names2) = names.splitAt(64) val matrix1 = new StringMatrix(names1) From f3d0fca9bd5a1eeb27441a9870de3a3dc2b7f71c Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Sat, 10 May 2025 13:04:41 +0200 Subject: [PATCH 232/311] Update dependencies (#1391) * Update Scala 3 to 3.3.6 * Update jsoniter-scala-core and jsoniter-scala-macros to 2.35.3 --- .github/workflows/ci.yml | 4 ++-- build.sbt | 4 ++-- project/BuildHelper.scala | 2 +- project/plugins.sbt | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e187b434..41fe7b27a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: fail-fast: false matrix: java: ['11', '21'] - scala: ['2.13.16', '3.3.5'] + scala: ['2.13.16', '3.3.6'] steps: - name: Checkout current branch uses: actions/checkout@v4.1.2 @@ -76,7 +76,7 @@ jobs: fail-fast: false matrix: java: ['11', '21'] - scala: ['2.12.20', '2.13.16', '3.3.5'] + scala: ['2.12.20', '2.13.16', '3.3.6'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch diff --git a/build.sbt b/build.sbt index 734fe1a75..5cb3ea0a0 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.35.2" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.35.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.35.3" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.35.3" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 3747d2af9..69f53923d 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -28,7 +28,7 @@ object BuildHelper { } val Scala212: String = versions("2.12") val Scala213: String = versions("2.13") - val ScalaDotty: String = "3.3.5" + val ScalaDotty: String = "3.3.6" val SilencerVersion = "1.7.19" diff --git a/project/plugins.sbt b/project/plugins.sbt index 8f846f15b..b425fdb22 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,4 @@ +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.3") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") From 6c49206a76543780b99e6e87539c6d849e7e8e7c Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sat, 10 May 2025 13:04:56 +0200 Subject: [PATCH 233/311] Update scalafmt-core to 3.9.6 (#1386) --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index c08b05508..4f9ac2bc0 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.9.5" +version = "3.9.6" runner.dialect = scala213 maxColumn = 120 align.preset = most From af0d41d9ea14ed304a87b196e96e01944e67d112 Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Thu, 15 May 2025 23:29:19 +1000 Subject: [PATCH 234/311] Add Scala Steward & Dependabot to help maintaining the repo (#1393) --- .github/dependabot.yml | 6 ++++++ .github/renovate.json | 13 ------------- .github/workflows/scala-steward.yml | 27 +++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 .github/renovate.json create mode 100644 .github/workflows/scala-steward.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..120c6893b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index 86e50ef6c..000000000 --- a/.github/renovate.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "automerge": true, - "rebaseWhen": "conflicted", - "labels": ["type: dependencies"], - "packageRules": [ - { - "matchManagers": [ - "sbt" - ], - "enabled": false - } - ] -} diff --git a/.github/workflows/scala-steward.yml b/.github/workflows/scala-steward.yml new file mode 100644 index 000000000..822b2a9cf --- /dev/null +++ b/.github/workflows/scala-steward.yml @@ -0,0 +1,27 @@ +name: Scala Steward + +# This workflow will launch every day at 00:00 +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: {} + +permissions: + contents: write + pull-requests: write + +jobs: + scala-steward: + timeout-minutes: 45 + runs-on: ubuntu-latest + name: Scala Steward + steps: + - name: Setup sbt + uses: sbt/setup-sbt@v1 + - name: Scala Steward + uses: scala-steward-org/scala-steward-action@v2 + with: + github-app-id: ${{ secrets.SCALA_STEWARD_GITHUB_APP_ID }} + github-app-installation-id: ${{ secrets.SCALA_STEWARD_GITHUB_APP_INSTALLATION_ID }} + github-app-key: ${{ secrets.SCALA_STEWARD_GITHUB_APP_PRIVATE_KEY }} + github-app-auth-only: true From 7afc9abcc403babea2cd5c01574e0e34d85acd6a Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Thu, 15 May 2025 23:30:17 +1000 Subject: [PATCH 235/311] Optimise `String::fromYaml` extension method (#1392) --- .../src/main/scala/zio/json/yaml/package.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/zio-json-yaml/src/main/scala/zio/json/yaml/package.scala b/zio-json-yaml/src/main/scala/zio/json/yaml/package.scala index 88d3a5517..683696518 100644 --- a/zio-json-yaml/src/main/scala/zio/json/yaml/package.scala +++ b/zio-json-yaml/src/main/scala/zio/json/yaml/package.scala @@ -16,7 +16,7 @@ import java.io.{ StringReader, StringWriter } import java.nio.charset.StandardCharsets import java.util.Base64 import scala.jdk.CollectionConverters._ -import scala.util.Try +import scala.util.control.NonFatal import scala.util.matching.Regex package object yaml { @@ -67,12 +67,12 @@ package object yaml { implicit final class DecoderYamlOps(private val raw: String) extends AnyVal { def fromYaml[A](implicit decoder: JsonDecoder[A]): Either[String, A] = - Try { + try { val yaml = new Yaml().compose(new StringReader(raw)) - yamlToJson(yaml) - }.toEither.left - .map(_.getMessage) - .flatMap(decoder.fromJsonAST(_)) + decoder.fromJsonAST(yamlToJson(yaml)) + } catch { + case NonFatal(e) => Left(e.getMessage) + } } private val multiline: Regex = "[\n\u0085\u2028\u2029]".r From 74b87f2fadc69c5a2d95e430550b666f83baa370 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 21:08:00 +0200 Subject: [PATCH 236/311] Bump actions/setup-node from 3 to 4 (#1397) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/site.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 26cd3895d..7ba7c510d 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -45,7 +45,7 @@ jobs: jvm: temurin:17 apps: sbt - name: Setup NodeJs - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.x registry-url: https://registry.npmjs.org From 56b5c978ecf6bb6c2d84cbd5cb365dae30987379 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 21:08:18 +0200 Subject: [PATCH 237/311] Bump release-drafter/release-drafter from 5 to 6 (#1396) Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 5 to 6. - [Release notes](https://github.com/release-drafter/release-drafter/releases) - [Commits](https://github.com/release-drafter/release-drafter/compare/v5...v6) --- updated-dependencies: - dependency-name: release-drafter/release-drafter dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 74d030b0a..80e062055 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -8,6 +8,6 @@ jobs: update_release_draft: runs-on: ubuntu-22.04 steps: - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 3c31e9544cb8b8e95ca5d0e3331c98a8a5f048a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 21:08:35 +0200 Subject: [PATCH 238/311] Bump hmarr/auto-approve-action from 3.2.0 to 4.0.0 (#1395) Bumps [hmarr/auto-approve-action](https://github.com/hmarr/auto-approve-action) from 3.2.0 to 4.0.0. - [Release notes](https://github.com/hmarr/auto-approve-action/releases) - [Commits](https://github.com/hmarr/auto-approve-action/compare/v3.2.0...v4.0.0) --- updated-dependencies: - dependency-name: hmarr/auto-approve-action dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-approve.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index df70adf48..1d7854f14 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -7,7 +7,7 @@ jobs: auto-approve: runs-on: ubuntu-22.04 steps: - - uses: hmarr/auto-approve-action@v3.2.0 + - uses: hmarr/auto-approve-action@v4.0.0 if: github.actor == 'scala-steward' || github.actor == 'renovate[bot]' with: github-token: "${{ secrets.GITHUB_TOKEN }}" From 988e439ef42d0f65c76bdd34b3cb17c559d16d64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 06:50:27 +0200 Subject: [PATCH 239/311] Bump actions/checkout from 3.3.0 to 4.2.2 (#1394) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.3.0 to 4.2.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.3.0...v4.2.2) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 4.2.2 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/site.yml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41fe7b27a..5171b2792 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout current branch - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.2.2 with: fetch-depth: 0 - name: Setup Action @@ -40,7 +40,7 @@ jobs: scala: ['2.13.16', '3.3.6'] steps: - name: Checkout current branch - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.2.2 with: fetch-depth: 0 - name: Setup Action @@ -58,7 +58,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout current branch - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.2.2 - name: Setup Action uses: coursier/setup-action@v1 with: @@ -80,7 +80,7 @@ jobs: platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.2.2 with: fetch-depth: 0 - name: Install Boehm GC @@ -107,7 +107,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout current branch - uses: actions/checkout@v4 + uses: actions/checkout@v4.2.2 with: fetch-depth: 300 - name: Fetch tags @@ -133,7 +133,7 @@ jobs: if: github.event_name != 'pull_request' steps: - name: Checkout current branch - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.2.2 with: fetch-depth: 0 - name: Setup Action diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 7ba7c510d..a3987b0f8 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -18,7 +18,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Git Checkout - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.2.2 with: fetch-depth: '0' - name: Setup Action @@ -36,7 +36,7 @@ jobs: if: ${{ ((github.event_name == 'release') && (github.event.action == 'published')) || (github.event_name == 'workflow_dispatch') }} steps: - name: Git Checkout - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.2.2 with: fetch-depth: '0' - name: Setup Action @@ -59,7 +59,7 @@ jobs: if: ${{ (github.event_name == 'push') || ((github.event_name == 'release') && (github.event.action == 'published')) }} steps: - name: Git Checkout - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v4.2.2 with: ref: ${{ github.head_ref }} fetch-depth: '0' From 817a6401841f3940bfb332f992c054adc33a3fa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 15:02:34 +1000 Subject: [PATCH 240/311] Bump peter-evans/create-pull-request from 4.2.3 to 7.0.8 (#1398) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.2.3 to 7.0.8. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v4.2.3...v7.0.8) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: 7.0.8 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/site.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index a3987b0f8..c15b6da04 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -75,7 +75,7 @@ jobs: git add README.md git commit -m "Update README.md" || echo "No changes to commit" - name: Create Pull Request - uses: peter-evans/create-pull-request@v4.2.3 + uses: peter-evans/create-pull-request@v7.0.8 with: body: |- Autogenerated changes after running the `sbt docs/generateReadme` command of the [zio-sbt-website](https://zio.dev/zio-sbt) plugin. From 1c0843a31fd488bce813ffe5cdc1df1b81d0bbef Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 19 May 2025 10:38:11 +0200 Subject: [PATCH 241/311] Update zio, zio-streams, zio-test, ... to 2.1.18 (#1399) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5cb3ea0a0..d0e783f37 100644 --- a/build.sbt +++ b/build.sbt @@ -58,7 +58,7 @@ addCommandAlias( "zioJsonMacrosNative/test" ) -val zioVersion = "2.1.17" +val zioVersion = "2.1.18" lazy val zioJsonRoot = project .in(file(".")) From f0f41e33df58cc73b4216d4b22beb411424dce44 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 05:44:41 +0200 Subject: [PATCH 242/311] Update magnolia to 1.3.18 (#1404) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d0e783f37..f79e43af3 100644 --- a/build.sbt +++ b/build.sbt @@ -124,7 +124,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) CrossVersion.partialVersion(scalaVersion.value) match { case Some((3, _)) => Seq( - "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.16" + "com.softwaremill.magnolia1_3" %%% "magnolia" % "1.3.18" ) case _ => Seq( From 564594f2f5bf0f3bfacc0c5a14363b73651403f2 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 05:22:32 +0200 Subject: [PATCH 243/311] Update jsoniter-scala-core, ... to 2.36.2 (#1406) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index f79e43af3..e1a86f190 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.35.3" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.35.3" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.2" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 9f175c50440404861c6c5f3e84a796ccc2845583 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 26 May 2025 20:29:55 +0200 Subject: [PATCH 244/311] Update sbt-ci-release to 1.11.0 (#1407) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index b425fdb22..a81788fef 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.3") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.0") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") From 627045f6b573c768f0d80c0e1f95ad17b3c534e1 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 26 May 2025 20:30:10 +0200 Subject: [PATCH 245/311] Update sbt, scripted-plugin to 1.11.0 (#1408) --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index cc68b53f1..6520f6981 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.11 +sbt.version=1.11.0 diff --git a/project/build.properties b/project/build.properties index cc68b53f1..6520f6981 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.11 +sbt.version=1.11.0 From 27ad36964df293d6ec3f0ad6db43a8e60f6257d3 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 20:30:25 +0200 Subject: [PATCH 246/311] Update sbt-ci-release to 1.11.0 (#1409) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> From d248da3f542ba6ed2c6fdf848ebeb4a28f1c6c58 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 20:30:46 +0200 Subject: [PATCH 247/311] Update sbt, scripted-plugin to 1.11.0 (#1410) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> From 0d1cfc2387ce3091fe2263c4512ad58ce6b79786 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 26 May 2025 20:31:00 +0200 Subject: [PATCH 248/311] Update magnolia to 1.3.18 (#1403) From 82f1a403674d237283357470e83d14b18b472c62 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 07:44:57 +0200 Subject: [PATCH 249/311] Update sbt-ci-release to 1.11.1 (#1414) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index a81788fef..6657c2a95 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.0") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.1") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") From 00731b518b2b1eb75f895054608a05db8397e744 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Mon, 2 Jun 2025 07:45:33 +0200 Subject: [PATCH 250/311] Update sbt-ci-release to 1.11.1 (#1415) From f7a19f471f6bd76462ed764d74f4145d4e88173b Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:44:59 +0200 Subject: [PATCH 251/311] Update zio, zio-streams, zio-test, ... to 2.1.19 (#1411) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index e1a86f190..cc5d7de8d 100644 --- a/build.sbt +++ b/build.sbt @@ -58,7 +58,7 @@ addCommandAlias( "zioJsonMacrosNative/test" ) -val zioVersion = "2.1.18" +val zioVersion = "2.1.19" lazy val zioJsonRoot = project .in(file(".")) From 371b82defc3becf007f8b4b34ae4646e782cb6d4 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:45:16 +0200 Subject: [PATCH 252/311] Update scalafmt-core to 3.9.7 (#1412) --- .git-blame-ignore-revs | 3 ++ .scalafmt.conf | 2 +- build.sbt | 24 +++++------ project/NeoJmhPlugin.scala | 2 +- .../main/scala/zio/json/golden/package.scala | 6 +-- .../src/main/scala/zio/json/jsonDerive.scala | 12 +++--- .../scala/zio/json/internal/SafeNumbers.scala | 4 +- .../scala/zio/json/internal/SafeNumbers.scala | 8 ++-- .../json/JsonEncoderPlatformSpecific.scala | 4 +- .../json/EncoderPlatformSpecificSpec.scala | 2 +- .../scala/zio/json/JsonTestSuiteSpec.scala | 2 +- .../scala/zio/json/data/geojson/GeoJSON.scala | 6 +-- .../src/main/scala-2.x/zio/json/macros.scala | 42 +++++++++---------- .../src/main/scala/zio/json/JsonCodec.scala | 2 +- .../src/main/scala/zio/json/JsonDecoder.scala | 2 +- .../src/main/scala/zio/json/JsonEncoder.scala | 6 +-- .../src/main/scala/zio/json/ast/ast.scala | 28 ++++++------- .../scala/zio/json/codegen/Generator.scala | 6 +-- .../main/scala/zio/json/internal/lexer.scala | 14 +++---- .../scala/zio/json/internal/readers.scala | 4 +- .../scala/zio/json/javatime/parsers.scala | 2 +- .../scala/zio/json/javatime/serializers.scala | 4 +- .../test/scala/zio/json/ast/JsonSpec.scala | 22 +++++----- .../zio/json/internal/StringMatrixSpec.scala | 10 ++--- 24 files changed, 110 insertions(+), 107 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 6923f3c94..5a7e61055 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -3,3 +3,6 @@ # Scala Steward: Reformat with scalafmt 3.9.0 47449d54fdda17becd0fce8efd14c894563773c0 + +# Scala Steward: Reformat with scalafmt 3.9.7 +d5280cb023facfd5af0f0a4eb456a848b63d0ee8 diff --git a/.scalafmt.conf b/.scalafmt.conf index 4f9ac2bc0..3724ac424 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.9.6" +version = "3.9.7" runner.dialect = scala213 maxColumn = 120 align.preset = most diff --git a/build.sbt b/build.sbt index cc5d7de8d..a43cc780d 100644 --- a/build.sbt +++ b/build.sbt @@ -12,7 +12,7 @@ inThisBuild( organization := "dev.zio", homepage := Some(url("https://zio.dev/zio-json/")), licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), - developers := List( + developers := List( Developer( "jdegoes", "John De Goes", @@ -134,12 +134,12 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) } }, Compile / sourceGenerators += Def.task { - val dir = (Compile / sourceManaged).value - val file = dir / "zio" / "json" / "GeneratedTupleDecoders.scala" + val dir = (Compile / sourceManaged).value + val file = dir / "zio" / "json" / "GeneratedTupleDecoders.scala" val decoders = (1 to 22).map { i => val tparams = (1 to i).map(p => s"A$p").mkString(", ") val implicits = (1 to i).map(p => s"A$p: JsonDecoder[A$p]").mkString(", ") - val work = (1 to i) + val work = (1 to i) .map(p => s"val a$p = A$p.unsafeDecode(traces(${p - 1}) :: trace, in)") .mkString("\n Lexer.char(trace, in, ',')\n ") val work2 = (1 to i) @@ -179,12 +179,12 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) Seq(file) }.taskValue, Compile / sourceGenerators += Def.task { - val dir = (Compile / sourceManaged).value - val file = dir / "zio" / "json" / "GeneratedTupleEncoders.scala" + val dir = (Compile / sourceManaged).value + val file = dir / "zio" / "json" / "GeneratedTupleEncoders.scala" val encoders = (1 to 22).map { i => val tparams = (1 to i).map(p => s"A$p").mkString(", ") val implicits = (1 to i).map(p => s"A$p: JsonEncoder[A$p]").mkString(", ") - val work = (1 to i) + val work = (1 to i) .map(p => s"A$p.unsafeEncode(t._$p, indent, out)") .mkString("\n if (indent.isEmpty) out.write(',') else out.write(\", \")\n ") s"""implicit def tuple$i[$tparams](implicit $implicits): JsonEncoder[Tuple$i[$tparams]] = @@ -207,8 +207,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) Seq(file) }.taskValue, Compile / sourceGenerators += Def.task { - val dir = (Compile / sourceManaged).value - val file = dir / "zio" / "json" / "GeneratedTupleCodecs.scala" + val dir = (Compile / sourceManaged).value + val file = dir / "zio" / "json" / "GeneratedTupleCodecs.scala" val codecs = (1 to 22).map { i => val tparamDecls = (1 to i).map(p => s"A$p: JsonEncoder: JsonDecoder").mkString(", ") val tparams = (1 to i).map(p => s"A$p").mkString(", ") @@ -389,9 +389,9 @@ lazy val docs = project crossScalaVersions -= ScalaDotty, moduleName := "zio-json-docs", scalacOptions += "-Ymacro-annotations", - projectName := "ZIO JSON", - mainModuleName := (zioJsonJVM / moduleName).value, - projectStage := ProjectStage.ProductionReady, + projectName := "ZIO JSON", + mainModuleName := (zioJsonJVM / moduleName).value, + projectStage := ProjectStage.ProductionReady, ScalaUnidoc / unidoc / unidocProjectFilter := inProjects( zioJsonJVM, zioJsonYaml, diff --git a/project/NeoJmhPlugin.scala b/project/NeoJmhPlugin.scala index 6edc79ed0..45f4571d1 100644 --- a/project/NeoJmhPlugin.scala +++ b/project/NeoJmhPlugin.scala @@ -123,7 +123,7 @@ object NeoJmhPlugin extends AutoPlugin { val javaFlags = (Jmh / javaOptions).value.toVector val inputs: Set[File] = (bytecodeDir ** "*").filter(_.isFile).get.toSet - val cachedGeneration = FileFunction.cached(cacheDir, FilesInfo.hash) { _ => + val cachedGeneration = FileFunction.cached(cacheDir, FilesInfo.hash) { _ => IO.delete(sourceDir) IO.createDirectory(sourceDir) IO.delete(resourceDir) diff --git a/zio-json-golden/src/main/scala/zio/json/golden/package.scala b/zio-json-golden/src/main/scala/zio/json/golden/package.scala index 561aae7bd..63da153ca 100644 --- a/zio-json-golden/src/main/scala/zio/json/golden/package.scala +++ b/zio-json-golden/src/main/scala/zio/json/golden/package.scala @@ -42,7 +42,7 @@ package object golden { resourceDir <- createGoldenDirectory(s"src/test/resources/golden/$relativePath") fileName = Paths.get(s"$name.json") filePath = resourceDir.resolve(fileName) - assertion <- ZIO.ifZIO(ZIO.attemptBlocking(Files.exists(filePath)))( + assertion <- ZIO.ifZIO(ZIO.attemptBlocking(Files.exists(filePath)))( validateTest(resourceDir, name, gen, sampleSize), createNewTest(resourceDir, name, gen, sampleSize) ) @@ -62,7 +62,7 @@ package object golden { for { currentSample <- readSampleFromFile(filePath) sample <- generateSample(gen, sampleSize) - assertion <- if (sample == currentSample) { + assertion <- if (sample == currentSample) { ZIO.succeed(assertTrue(sample == currentSample)) } else { val diffFileName = Paths.get(s"${name}_changed.json") @@ -87,7 +87,7 @@ package object golden { for { sample <- generateSample(gen, sampleSize) - _ <- + _ <- ZIO .ifZIO(ZIO.attemptBlocking(Files.exists(filePath)))(ZIO.unit, ZIO.attemptBlocking(Files.createFile(filePath))) _ <- writeSampleToFile(filePath, sample) diff --git a/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala b/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala index f92704319..7d16f549f 100644 --- a/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala +++ b/zio-json-macros/shared/src/main/scala/zio/json/jsonDerive.scala @@ -113,11 +113,11 @@ private[json] final class DeriveCodecMacros(val c: blackbox.Context) { } private[this] def codec(clsDef: ClassDef): Tree = { - val tpname = clsDef.name - val tparams = clsDef.tparams - val decoderName = TermName("decode" + tpname.decodedName) - val encoderName = TermName("encode" + tpname.decodedName) - val codecName = TermName("codecFor" + tpname.decodedName) + val tpname = clsDef.name + val tparams = clsDef.tparams + val decoderName = TermName("decode" + tpname.decodedName) + val encoderName = TermName("encode" + tpname.decodedName) + val codecName = TermName("codecFor" + tpname.decodedName) val (decoder, encoder, codec) = if (tparams.isEmpty) { val Type = tpname ( @@ -126,7 +126,7 @@ private[json] final class DeriveCodecMacros(val c: blackbox.Context) { q"""implicit val $codecName: $CodecClass[$Type] = _root_.zio.json.DeriveJsonCodec.gen[$Type]""" ) } else { - val tparamNames = tparams.map(_.name) + val tparamNames = tparams.map(_.name) def mkImplicitParams(prefix: String, typeSymbol: TypeSymbol) = tparamNames.map { var i = 0 diff --git a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala index b6f0403cc..04af508a8 100644 --- a/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -157,7 +157,7 @@ object SafeNumbers { exp } } else { - val n = calculateTenPow18SquareNumber(bitLen) + val n = calculateTenPow18SquareNumber(bitLen) val ss1 = if (ss eq null) getTenPow18Squares(n) else ss @@ -226,7 +226,7 @@ object SafeNumbers { val bitLen = x.bitLength if (bitLen < 64) write(x.longValue, out) else { - val n = calculateTenPow18SquareNumber(bitLen) + val n = calculateTenPow18SquareNumber(bitLen) val ss1 = if (ss eq null) getTenPow18Squares(n) else ss diff --git a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala index 40f08be75..cbc4bb1c6 100644 --- a/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala +++ b/zio-json/jvm-native/src/main/scala/zio/json/internal/SafeNumbers.scala @@ -130,8 +130,8 @@ object SafeNumbers { ): Int = { val bitLen = x.bitLength if (bitLen < 64) { - val v = x.longValue - val pv = Math.abs(v) + val v = x.longValue + val pv = Math.abs(v) val digits = if (pv >= 100000000000000000L) { if (pv >= 1000000000000000000L) 19 @@ -161,7 +161,7 @@ object SafeNumbers { exp } } else { - val n = calculateTenPow18SquareNumber(bitLen) + val n = calculateTenPow18SquareNumber(bitLen) val ss1 = if (ss eq null) getTenPow18Squares(n) else ss @@ -230,7 +230,7 @@ object SafeNumbers { val bitLen = x.bitLength if (bitLen < 64) write(x.longValue, out) else { - val n = calculateTenPow18SquareNumber(bitLen) + val n = calculateTenPow18SquareNumber(bitLen) val ss1 = if (ss eq null) getTenPow18Squares(n) else ss diff --git a/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala b/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala index 60b7d15ff..3793ec0cc 100644 --- a/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala +++ b/zio-json/jvm/src/main/scala/zio/json/JsonEncoderPlatformSpecific.scala @@ -24,7 +24,7 @@ trait JsonEncoderPlatformSpecific[A] { self: JsonEncoder[A] => for { runtime <- ZIO.runtime[Any] chunkBuffer <- Ref.make(Chunk.fromIterable(startWith.toList)) - writer <- ZIO.fromAutoCloseable { + writer <- ZIO.fromAutoCloseable { ZIO.succeed { new java.io.BufferedWriter( new java.io.Writer { @@ -45,7 +45,7 @@ trait JsonEncoderPlatformSpecific[A] { self: JsonEncoder[A] => } writeWriter <- ZIO.succeed(new WriteWriter(writer)) hasAtLeastOneElement <- Ref.make(false) - push = { (is: Option[Chunk[A]]) => + push = { (is: Option[Chunk[A]]) => val pushChars = chunkBuffer.getAndUpdate(c => if (c.isEmpty) c else Chunk()) is match { diff --git a/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala b/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala index 02e7a2111..8d50d34e9 100644 --- a/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala +++ b/zio-json/jvm/src/test/scala/zio/json/EncoderPlatformSpecificSpec.scala @@ -87,7 +87,7 @@ object EncoderPlatformSpecificSpec extends ZIOSpecDefault { ), suite("helpers in zio.json")( test("writeJsonLines writes JSON lines") { - val path = Files.createTempFile("log", "json") + val path = Files.createTempFile("log", "json") val events = Chunk( Event(1, "hello", priority = 1111.1111111), Event(12, "hello", priority = 11111111.111), diff --git a/zio-json/jvm/src/test/scala/zio/json/JsonTestSuiteSpec.scala b/zio-json/jvm/src/test/scala/zio/json/JsonTestSuiteSpec.scala index d2408b62f..f3883e797 100644 --- a/zio-json/jvm/src/test/scala/zio/json/JsonTestSuiteSpec.scala +++ b/zio-json/jvm/src/test/scala/zio/json/JsonTestSuiteSpec.scala @@ -20,7 +20,7 @@ object JsonTestSuiteSpec extends ZIOSpecDefault { a <- ZIO.foreach(f.sorted) { path => for { input <- getResourceAsStringM(s"json_test_suite/$path") - exit <- ZIO.succeed { + exit <- ZIO.succeed { // Catch Stack overflow try { JsonDecoder[Json] diff --git a/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala index b827fd66b..9374f0733 100644 --- a/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala +++ b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala @@ -135,7 +135,7 @@ package handrolled { val names: Array[String] = Array("type", "coordinates", "geometries") val matrix: StringMatrix = new StringMatrix(names) val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_)) - val subtypes: StringMatrix = new StringMatrix( + val subtypes: StringMatrix = new StringMatrix( Array( "Point", "MultiPoint", @@ -286,12 +286,12 @@ package handrolled { Array("type", "properties", "geometry", "features") val matrix: StringMatrix = new StringMatrix(names) val spans: Array[JsonError] = names.map(JsonError.ObjectAccess(_)) - val subtypes: StringMatrix = new StringMatrix( + val subtypes: StringMatrix = new StringMatrix( Array("Feature", "FeatureCollection") ) val propertyD: JsonDecoder[Map[String, String]] = JsonDecoder[Map[String, String]] - val geometryD: JsonDecoder[Geometry] = JsonDecoder[Geometry] + val geometryD: JsonDecoder[Geometry] = JsonDecoder[Geometry] lazy val featuresD: JsonDecoder[List[GeoJSON]] = JsonDecoder[List[GeoJSON]] // recursive diff --git a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala index 913f7d261..6a13b1b50 100644 --- a/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala +++ b/zio-json/shared/src/main/scala-2.x/zio/json/macros.scala @@ -94,7 +94,7 @@ private[json] object jsonMemberNames { if (s.indexOf('_') == -1 && s.indexOf('-') == -1) { if (s.isEmpty) s else { - val ch = s.charAt(0) + val ch = s.charAt(0) val fixedCh = if (toPascal) toUpperCase(ch) else toLowerCase(ch) @@ -226,7 +226,7 @@ object DeriveJsonDecoder { }.isDefined || !config.allowExtraFields if (ctx.parameters.isEmpty) new CaseObjectDecoder(ctx, no_extra) else { - var splitIndex = -1 + var splitIndex = -1 val (names, aliases): (Array[String], Array[(String, Int)]) = { val names = new Array[String](ctx.parameters.size) val aliasesBuilder = new ArrayBuffer[(String, Int)] @@ -244,7 +244,7 @@ object DeriveJsonDecoder { val aliases = aliasesBuilder.toArray val allFieldNames = names ++ aliases.map(_._1) if (allFieldNames.length != allFieldNames.distinct.length) { - val typeName = ctx.typeName.full + val typeName = ctx.typeName.full val collisions = aliases .map(_._1) .distinct @@ -258,12 +258,12 @@ object DeriveJsonDecoder { } if (splitIndex < 0) { new CollectionJsonDecoder[A] { - private[this] val len = names.length - private[this] val matrix = new StringMatrix(names, aliases) - private[this] val spans = names.map(JsonError.ObjectAccess) - private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray - private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] - private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap + private[this] val len = names.length + private[this] val matrix = new StringMatrix(names, aliases) + private[this] val spans = names.map(JsonError.ObjectAccess) + private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray + private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] + private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap private[this] val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.decoding @@ -384,18 +384,18 @@ object DeriveJsonDecoder { } else { val (names1, names2) = names.splitAt(splitIndex) val aliases1 = aliases.filter(kv => kv._2 <= splitIndex) - val aliases2 = aliases.collect { + val aliases2 = aliases.collect { case (k, v) if v > splitIndex => (k, v - splitIndex) } new CollectionJsonDecoder[A] { - private[this] val len = names.length - private[this] val matrix1 = new StringMatrix(names1, aliases1) - private[this] val matrix2 = new StringMatrix(names2, aliases2) - private[this] val spans = names.map(JsonError.ObjectAccess) - private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray - private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] - private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap + private[this] val len = names.length + private[this] val matrix1 = new StringMatrix(names1, aliases1) + private[this] val matrix2 = new StringMatrix(names2, aliases2) + private[this] val spans = names.map(JsonError.ObjectAccess) + private[this] val defaults = ctx.parameters.map(_.evaluateDefault.orNull).toArray + private[this] lazy val tcs = ctx.parameters.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] + private[this] lazy val namesMap = (names.zipWithIndex ++ aliases).toMap private[this] val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.decoding @@ -530,12 +530,12 @@ object DeriveJsonDecoder { } val (names1, names2) = names.splitAt(64) val matrix1 = new StringMatrix(names1) - val matrix2 = + val matrix2 = if (names2.isEmpty) null else new StringMatrix(names2) lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonDecoder[Any]]] lazy val namesMap = names.zipWithIndex.toMap - val discrim = + val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) lazy val isEnumeration = config.enumValuesAsStrings && ctx.subtypes.forall(_.typeclass.isInstanceOf[CaseObjectDecoder[JsonDecoder, _]]) @@ -738,7 +738,7 @@ object DeriveJsonEncoder { else { val nameTransform = ctx.annotations.collectFirst { case jsonMemberNames(format) => format }.getOrElse(config.fieldNameMapping) - val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) + val explicitNulls = config.explicitNulls || ctx.annotations.exists(_.isInstanceOf[jsonExplicitNull]) val explicitEmptyCollections = ctx.annotations.collectFirst { case a: jsonExplicitEmptyCollections => a.encoding } .getOrElse(config.explicitEmptyCollections.encoding) val params = ctx.parameters.filter(p => p.annotations.collectFirst { case _: jsonExclude => () }.isEmpty).toArray @@ -811,7 +811,7 @@ object DeriveJsonEncoder { }.toArray val encodedNames: Array[String] = names.map(name => JsonEncoder.string.encodeJson(name, None).toString) lazy val tcs = ctx.subtypes.map(_.typeclass).toArray.asInstanceOf[Array[JsonEncoder[Any]]] - val discrim = + val discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }.orElse(config.sumTypeHandling.discriminatorField) lazy val isEnumeration = config.enumValuesAsStrings && ctx.subtypes.forall(_.typeclass == caseObjectEncoder) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala index ccd6ece13..77ffccb2a 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonCodec.scala @@ -138,7 +138,7 @@ private[json] trait CodecLowPriority0 extends CodecLowPriority1 { this: JsonCode } private[json] trait CodecLowPriority1 extends CodecLowPriority2 { this: JsonCodec.type => - implicit def seq[A: JsonEncoder: JsonDecoder]: JsonCodec[Seq[A]] = JsonCodec(JsonEncoder.seq[A], JsonDecoder.seq[A]) + implicit def seq[A: JsonEncoder: JsonDecoder]: JsonCodec[Seq[A]] = JsonCodec(JsonEncoder.seq[A], JsonDecoder.seq[A]) implicit def list[A: JsonEncoder: JsonDecoder]: JsonCodec[List[A]] = JsonCodec(JsonEncoder.list[A], JsonDecoder.list[A]) implicit def vector[A: JsonEncoder: JsonDecoder]: JsonCodec[Vector[A]] = diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 458961837..1b3da62ed 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -705,7 +705,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with private[json] trait CollectionJsonDecoder[A] extends JsonDecoder[A] private[json] trait OptionJsonDecoder[A] extends JsonDecoder[A] -private[json] trait MappedJsonDecoder[A] extends JsonDecoder[A] { +private[json] trait MappedJsonDecoder[A] extends JsonDecoder[A] { private[json] def underlying: JsonDecoder[_] } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala index 01d513f9f..55744aa7b 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala @@ -176,7 +176,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with case '\n' => out.write('\\', 'n') case '\r' => out.write('\\', 'r') case '\t' => out.write('\\', 't') - case c => + case c => if (c >= ' ') out.write(c) else { out.write('\\', 'u') @@ -199,7 +199,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with case '\n' => out.write('"', '\\', 'n', '"') case '\r' => out.write('"', '\\', 'r', '"') case '\t' => out.write('"', '\\', 't', '"') - case c => + case c => if (c >= ' ') out.write('"', c, '"') else { out.write('"', '\\', 'u') @@ -251,7 +251,7 @@ object JsonEncoder extends GeneratedTupleEncoders with EncoderLowPriority1 with override def toJsonAST(a: Boolean): Either[String, Json] = new Right(Json.Bool(a)) } implicit val symbol: JsonEncoder[Symbol] = string.contramap(_.name) - implicit val byte: JsonEncoder[Byte] = new JsonEncoder[Byte] { + implicit val byte: JsonEncoder[Byte] = new JsonEncoder[Byte] { def unsafeEncode(a: Byte, indent: Option[Int], out: Write): Unit = SafeNumbers.write(a.toInt, out) override def toJsonAST(a: Byte): Either[String, Json] = new Right(Json.Num(a.toInt)) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index 5f4ed7c90..f5f6d1252 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -170,7 +170,7 @@ sealed abstract class Json { self => case s: Str => s.value.hashCode case n: Num => n.value.hashCode case b: Bool => b.value.hashCode - case o: Obj => + case o: Obj => var result = 0 o.fields.foreach(tuple => result = result ^ tuple.hashCode) result @@ -337,7 +337,7 @@ object Json { def mapValues(f: Json => Json): Json.Obj = Json.Obj(fields.map(e => e._1 -> f(e._2))) def filter(pred: ((String, Json)) => Boolean): Json.Obj = Json.Obj(fields.filter(pred)) def filterKeys(pred: String => Boolean): Json.Obj = Json.Obj(fields.filter(e => pred(e._1))) - def merge(that: Json.Obj): Json.Obj = { + def merge(that: Json.Obj): Json.Obj = { val fields1 = this.fields val fields2 = that.fields val leftMap = fields1.toMap @@ -386,7 +386,7 @@ object Json { if (fields.isEmpty) Obj.empty else new Obj(Chunk(fields: _*)) - private lazy val objd = JsonDecoder.keyValueChunk[String, Json] + private lazy val objd = JsonDecoder.keyValueChunk[String, Json] implicit val decoder: JsonDecoder[Obj] = new JsonDecoder[Obj] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Obj = Obj(objd.unsafeDecode(trace, in)) @@ -397,7 +397,7 @@ object Json { case _ => Lexer.error("Not an object", trace) } } - private lazy val obje = JsonEncoder.keyValueChunk[String, Json] + private lazy val obje = JsonEncoder.keyValueChunk[String, Json] implicit val encoder: JsonEncoder[Obj] = new JsonEncoder[Obj] { def unsafeEncode(a: Obj, indent: Option[Int], out: Write): Unit = obje.unsafeEncode(a.fields, indent, out) @@ -439,7 +439,7 @@ object Json { if (elements.isEmpty) empty else new Arr(Chunk(elements: _*)) - private lazy val arrd = JsonDecoder.chunk[Json] + private lazy val arrd = JsonDecoder.chunk[Json] implicit val decoder: JsonDecoder[Arr] = new JsonDecoder[Arr] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Arr = Arr(arrd.unsafeDecode(trace, in)) @@ -450,7 +450,7 @@ object Json { case _ => Lexer.error("Not an array", trace) } } - private lazy val arre = JsonEncoder.chunk[Json] + private lazy val arre = JsonEncoder.chunk[Json] implicit val encoder: JsonEncoder[Arr] = new JsonEncoder[Arr] { def unsafeEncode(a: Arr, indent: Option[Int], out: Write): Unit = arre.unsafeEncode(a.elements, indent, out) @@ -523,7 +523,7 @@ object Json { object Num { @inline def apply(value: Byte): Num = apply(value.toInt) @inline def apply(value: Short): Num = apply(value.toInt) - def apply(value: Int): Num = new Num({ + def apply(value: Int): Num = new Num({ if (value < 512 && value > -512) new java.math.BigDecimal(value) else BigDecimal(value).bigDecimal }) @@ -532,7 +532,7 @@ object Json { else BigDecimal(value).bigDecimal }) @inline def apply(value: BigDecimal): Num = new Num(value.bigDecimal) - def apply(value: BigInt): Num = + def apply(value: BigInt): Num = if (value.isValidLong) apply(value.toLong) else new Json.Num(new java.math.BigDecimal(value.bigInteger)) def apply(value: java.math.BigInteger): Num = @@ -562,7 +562,7 @@ object Json { } type Null = Null.type case object Null extends Json { - private[this] val nullChars: Array[Char] = "null".toCharArray + private[this] val nullChars: Array[Char] = "null".toCharArray implicit val decoder: JsonDecoder[Null.type] = new JsonDecoder[Null.type] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Null.type = { Lexer.readChars(trace, in, nullChars, "null") @@ -591,11 +591,11 @@ object Json { val c = in.nextNonWhitespace() in.retract() (c: @switch) match { - case 'n' => Null.decoder.unsafeDecode(trace, in) - case 'f' | 't' => Bool.decoder.unsafeDecode(trace, in) - case '{' => Obj.decoder.unsafeDecode(trace, in) - case '[' => Arr.decoder.unsafeDecode(trace, in) - case '"' => Str.decoder.unsafeDecode(trace, in) + case 'n' => Null.decoder.unsafeDecode(trace, in) + case 'f' | 't' => Bool.decoder.unsafeDecode(trace, in) + case '{' => Obj.decoder.unsafeDecode(trace, in) + case '[' => Arr.decoder.unsafeDecode(trace, in) + case '"' => Str.decoder.unsafeDecode(trace, in) case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => Num.decoder.unsafeDecode(trace, in) case c => diff --git a/zio-json/shared/src/main/scala/zio/json/codegen/Generator.scala b/zio-json/shared/src/main/scala/zio/json/codegen/Generator.scala index 70e4d281f..5f908265c 100644 --- a/zio-json/shared/src/main/scala/zio/json/codegen/Generator.scala +++ b/zio-json/shared/src/main/scala/zio/json/codegen/Generator.scala @@ -90,7 +90,7 @@ private[codegen] sealed trait JsonType extends Product with Serializable { self case (JNull, right) => JOption(right) case (left, JNull) => JOption(left) - case (JArray(left), JArray(right)) => JArray(left unify right) + case (JArray(left), JArray(right)) => JArray(left unify right) case (CaseClass(left, leftFields), CaseClass(right, rightFields)) if left == right => CaseClass(left, (leftFields unify rightFields).asInstanceOf[JObject]) case (left, right) => @@ -206,10 +206,10 @@ object ${clazz.name} { def unifyTypes(json: Json, key: Option[String] = None): JsonType = json match { - case Json.Null => JNull + case Json.Null => JNull case Json.Arr(elements) => JArray(elements.map(unifyTypes(_, key)).reduce(_ unify _)) - case Json.Bool(_) => JBoolean + case Json.Bool(_) => JBoolean case Json.Str(string) => val localDateTime = Try(LocalDateTime.parse(string, DateTimeFormatter.ISO_DATE_TIME)).toOption.map(_ => JLocalDateTime) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala index ddf1e7416..3e42b8958 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/lexer.scala @@ -130,7 +130,7 @@ object Lexer { case 'f' => skipFixedChars(in, 4) case '{' => skipObject(in, 0) case '[' => skipArray(in, 0) - case '"' => + case '"' => skipString(in, evenBackSlashes = true) case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => skipNumber(in) @@ -351,7 +351,7 @@ object Lexer { c1 == '-' && c2 == '-' && c3 == '-' && c4 == '-' } ) { - val ds = hexDigits + val ds = hexDigits val msb1 = ds(cs(0)).toLong << 28 | (ds(cs(1)) << 24 | @@ -1471,7 +1471,7 @@ object Lexer { } } val localDateTime = LocalDateTime.of(year, month, day, hour, minute, second, nano) - val zoneOffset = + val zoneOffset = if (ch == 'Z') { if (pos < i) { ch = cs(pos) @@ -1862,9 +1862,9 @@ object Lexer { // of strings, and a sequence of incoming characters, find the strings that // match, by manually maintaining a bitset. Empty strings are not allowed. final class StringMatrix(names: Array[String], aliases: Array[(String, Int)] = Array.empty) { - val namesLen: Int = names.length - private[this] val width: Int = namesLen + aliases.length - val initial: Long = -1L >>> (64 - width) + val namesLen: Int = names.length + private[this] val width: Int = namesLen + aliases.length + val initial: Long = -1L >>> (64 - width) private[this] val lengths: Array[Int] = { require(namesLen > 0 && width <= 64) val ls = new Array[Int](width) @@ -1883,7 +1883,7 @@ final class StringMatrix(names: Array[String], aliases: Array[(String, Int)] = A } ls } - private[this] val height: Int = lengths.max + private[this] val height: Int = lengths.max private[this] val matrix: Array[Char] = { val w = width val m = new Array[Char](height * w) diff --git a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala index 9328fa30e..b12228aab 100644 --- a/zio-json/shared/src/main/scala/zio/json/internal/readers.scala +++ b/zio-json/shared/src/main/scala/zio/json/internal/readers.scala @@ -86,8 +86,8 @@ sealed trait RetractReader extends OneCharReader { } final class FastCharSequence(s: Array[Char]) extends CharSequence { - def length: Int = s.length - def charAt(i: Int): Char = s(i) + def length: Int = s.length + def charAt(i: Int): Char = s(i) def subSequence(start: Int, end: Int): CharSequence = new FastCharSequence(Arrays.copyOfRange(s, start, end)) } diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala index 0af67595f..83904a81f 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/parsers.scala @@ -1006,7 +1006,7 @@ private[json] object parsers { } } val localDateTime = LocalDateTime.of(year, month, day, hour, minute, second, nano) - val zoneOffset = + val zoneOffset = if (ch == 'Z') { if (pos < len) { ch = input.charAt(pos) diff --git a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala index 89671f1cd..8b739cfb4 100644 --- a/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala +++ b/zio-json/shared/src/main/scala/zio/json/javatime/serializers.scala @@ -66,7 +66,7 @@ private[json] object serializers { def write(x: Instant, out: Write): Unit = { val epochSecond = x.getEpochSecond - val epochDay = + val epochDay = (if (epochSecond >= 0) epochSecond else epochSecond - 86399) / 86400 // 86400 == seconds per day val secsOfDay = (epochSecond - epochDay * 86400).toInt @@ -85,7 +85,7 @@ private[json] object serializers { } val marchMonth = (marchDayOfYear * 17135 + 6854) >> 19 // (marchDayOfYear * 5 + 2) / 153 year += (marchMonth * 3277 >> 15) + adjustYear // year += marchMonth / 10 + adjustYear (reset any negative year and convert march-based values back to january-based) - val month = marchMonth + + val month = marchMonth + (if (marchMonth < 10) 3 else -9) val day = diff --git a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala index a375ca08e..b7379e496 100644 --- a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala @@ -65,7 +65,7 @@ object JsonSpec extends ZIOSpecDefault { isRight( equalTo( Json.Obj( - "id" -> Json.Num(8500), + "id" -> Json.Num(8500), "user" -> Json.Obj( "id" -> Json.Num(6200), "name" -> Json.Str("Twitter API") @@ -83,7 +83,7 @@ object JsonSpec extends ZIOSpecDefault { isRight( equalTo( Json.Obj( - "id" -> Json.Num(8500), + "id" -> Json.Num(8500), "user" -> Json.Obj( "name" -> Json.Str("Twitter API") ), @@ -111,7 +111,7 @@ object JsonSpec extends ZIOSpecDefault { val str: Json = Json.Str("hello") val bool: Json = Json.Bool(true) val arr: Json = Json.Arr(nul, num, str) - val obj: Json = Json.Obj( + val obj: Json = Json.Obj( "nul" -> nul, "num" -> num, "str" -> str, @@ -212,7 +212,7 @@ object JsonSpec extends ZIOSpecDefault { val obj = Json.Obj( "one" -> Json.Obj( - "two" -> Json.Bool(true), + "two" -> Json.Bool(true), "three" -> Json.Obj( "four" -> Json.Null, "five" -> Json.Obj( @@ -287,7 +287,7 @@ object JsonSpec extends ZIOSpecDefault { }, test(">>>, array, filterType (second operand of >>> is complex)") { val downEntities = JsonCursor.field("entities") - val downHashtag = + val downHashtag = JsonCursor.isObject >>> JsonCursor.field("hashtags") >>> JsonCursor.isArray >>> JsonCursor.element(0) val combined = downEntities >>> downHashtag @@ -345,7 +345,7 @@ object JsonSpec extends ZIOSpecDefault { test("object, deep") { val intersected = tweet.intersect( Json.Obj( - "id" -> Json.Num(8501), + "id" -> Json.Num(8501), "user" -> Json.Obj( "id" -> Json.Num(6200), "name" -> Json.Str("Twitter API") @@ -469,7 +469,7 @@ object JsonSpec extends ZIOSpecDefault { assert(merged)( equalTo( Json.Obj( - "id" -> Json.Num(8500), + "id" -> Json.Num(8500), "user" -> Json.Obj( "id" -> Json.Num(6200), "name" -> Json.Str("Twitter API"), @@ -525,7 +525,7 @@ object JsonSpec extends ZIOSpecDefault { isRight( equalTo( Json.Obj( - "id" -> Json.Num(8500), + "id" -> Json.Num(8500), "entities" -> Json.Obj( "id" -> Json.Num(6200), "name" -> Json.Str("Twitter API") @@ -571,7 +571,7 @@ object JsonSpec extends ZIOSpecDefault { isRight( equalTo( Json.Obj( - "id" -> Json.Num(8500), + "id" -> Json.Num(8500), "user" -> Json.Obj( "id" -> Json.Num(6200), "name" -> Json.Str("Twitter API") @@ -590,7 +590,7 @@ object JsonSpec extends ZIOSpecDefault { isRight( equalTo( Json.Obj( - "id" -> Json.Num(8500), + "id" -> Json.Num(8500), "user" -> Json.Obj( "id" -> Json.Num(6201), "name" -> Json.Str("Twitter API") @@ -616,7 +616,7 @@ object JsonSpec extends ZIOSpecDefault { lazy val tweet: Json.Obj = Json.Obj( - "id" -> Json.Num(8500), + "id" -> Json.Num(8500), "user" -> Json.Obj( "id" -> Json.Num(6200), "name" -> Json.Str("Twitter API") diff --git a/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala b/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala index 8553f2b32..243d70550 100644 --- a/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/internal/StringMatrixSpec.scala @@ -44,10 +44,10 @@ object StringMatrixSpec extends ZIOSpecDefault { }, test("first resolves to field index") { check(genTestStrings) { xs => - val m = new StringMatrix(xs) + val m = new StringMatrix(xs) val asserts = xs.indices.map { i => val test = xs(i) - var bs = test.zipWithIndex.foldLeft(m.initial) { case (bs, (c, i)) => + var bs = test.zipWithIndex.foldLeft(m.initial) { case (bs, (c, i)) => m.update(bs, i, c) } bs = m.exact(bs, test.length) @@ -98,10 +98,10 @@ object StringMatrixSpec extends ZIOSpecDefault { }, test("alias first resolves to aliased field index") { check(genTestStringsAndAliases) { case (xs, aliases) => - val m = new StringMatrix(xs, aliases) + val m = new StringMatrix(xs, aliases) val asserts = aliases.indices.map { i => val test = aliases(i)._1 - var bs = test.zipWithIndex.foldLeft(m.initial) { case (bs, (c, i)) => + var bs = test.zipWithIndex.foldLeft(m.initial) { case (bs, (c, i)) => m.update(bs, i, c) } bs = m.exact(bs, test.length) @@ -131,7 +131,7 @@ object StringMatrixSpec extends ZIOSpecDefault { } yield (xs.toArray, aliasF zip aliasN) private def matcher(xs: Array[String], aliases: Array[(String, Int)], test: String): Array[String] = { - val m = new StringMatrix(xs, aliases) + val m = new StringMatrix(xs, aliases) var bs = test.foldLeft(m.initial) { var i = 0 (bs, c) => From da8e391d5fec57803ac595a167479d646ab4f7a0 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:45:40 +0200 Subject: [PATCH 253/311] Update sbt, scripted-plugin to 1.11.1 (#1416) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index 6520f6981..61c9b1cb1 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.0 +sbt.version=1.11.1 diff --git a/project/build.properties b/project/build.properties index 6520f6981..61c9b1cb1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.0 +sbt.version=1.11.1 From 1829b395e4a58debe7e16f3530de199202eed619 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:45:57 +0200 Subject: [PATCH 254/311] Update zio, zio-streams, zio-test, ... to 2.1.19 (#1413) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> From 55b3ace3c8b1c734f435d5d9ac15bbf7681cc239 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 3 Jun 2025 10:14:19 +0200 Subject: [PATCH 255/311] Update zio-sbt-website to 0.4.0-alpha.32 (#1417) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 6657c2a95..e8ccb5b72 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -11,6 +11,6 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") -addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.31") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.32") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.9" From 4994f710caa0de8e1d1740af50adbb1d69a0b96e Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Tue, 3 Jun 2025 11:31:25 +0200 Subject: [PATCH 256/311] Override publishing URLs (#1418) --- .github/workflows/ci.yml | 2 +- project/BuildHelper.scala | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5171b2792..3b1b46907 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,7 +130,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 60 needs: [ci] - if: github.event_name != 'pull_request' + if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout current branch uses: actions/checkout@v4.2.2 diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 69f53923d..ea5619a93 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -223,6 +223,11 @@ object BuildHelper { name := s"$prjName", crossScalaVersions := Seq(Scala212, Scala213, ScalaDotty), ThisBuild / scalaVersion := Scala213, + ThisBuild / publishTo := { + val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/" + if (isSnapshot.value) Some("central-snapshots" at centralSnapshots) + else localStaging.value + }, scalacOptions ++= stdOptions ++ extraOptions(scalaVersion.value, optimize = !isSnapshot.value), libraryDependencies ++= { if (scalaVersion.value == ScalaDotty) From 27f88fbef10d957a7b1be52cd557b7504c42869e Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 8 Jun 2025 02:20:41 +0200 Subject: [PATCH 257/311] Update auxlib, clib, javalib, nativelib, ... to 0.5.8 (#1422) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index e8ccb5b72..c991f6c51 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,7 +7,7 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.8") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") From 51ef008ac975544d8cfbebacf956b2045f9427a2 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 8 Jun 2025 02:20:57 +0200 Subject: [PATCH 258/311] Update jsoniter-scala-core, ... to 2.36.3 (#1421) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index a43cc780d..5c661473a 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.2" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.3" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.3" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 0972ff8339f0031f55f17b8c1b7509d6f5cdb190 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Sun, 8 Jun 2025 07:15:45 +0200 Subject: [PATCH 259/311] Update sbt, scripted-plugin to 1.11.2 (#1424) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index 61c9b1cb1..bbb0b608c 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.1 +sbt.version=1.11.2 diff --git a/project/build.properties b/project/build.properties index 61c9b1cb1..bbb0b608c 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.1 +sbt.version=1.11.2 From f38263843aa00d811c10f6fc5c7378a67f8754dc Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Sun, 8 Jun 2025 07:42:50 +0200 Subject: [PATCH 260/311] Update jsoniter-scala-core, ... to 2.36.3 (#1419) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> From 3485dda3099f9ac43a5d9907bd7625b667fd61e0 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Wed, 11 Jun 2025 06:53:06 +0200 Subject: [PATCH 261/311] Update jsoniter-scala-core, ... to 2.36.4 (#1426) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 5c661473a..71ccbf1f1 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.3" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.3" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.4" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.4" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 818e9cb18cb8b33e4a5556b69eb0cd33727e8ab1 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 13:31:19 +0200 Subject: [PATCH 262/311] Update circe-core, circe-generic, ... to 0.14.14 (#1427) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 71ccbf1f1..dce83f854 100644 --- a/build.sbt +++ b/build.sbt @@ -86,7 +86,7 @@ lazy val zioJsonRoot = project zioJsonGolden ) -val circeVersion = "0.14.13" +val circeVersion = "0.14.14" lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("zio-json")) From 60046e407747cbfe92cedb058dad97d06b190466 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 13:32:02 +0200 Subject: [PATCH 263/311] Update jsoniter-scala-core, ... to 2.36.5 (#1429) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index dce83f854..4c74b7c8c 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.4" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.4" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.5" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.5" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 1fb790f7731a322640859d7559c886c1d93629ee Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Thu, 19 Jun 2025 16:34:56 +1000 Subject: [PATCH 264/311] Add `Json.Obj(key, value)` constructor (#1430) --- .../src/main/scala/zio/json/ast/ast.scala | 3 + .../test/scala/zio/json/ast/JsonSpec.scala | 62 ++++++++++++------- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala index f5f6d1252..24aa303bd 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/ast.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/ast.scala @@ -386,6 +386,9 @@ object Json { if (fields.isEmpty) Obj.empty else new Obj(Chunk(fields: _*)) + def apply(key: String, value: Json): Obj = + new Obj(Chunk.single(key -> value)) + private lazy val objd = JsonDecoder.keyValueChunk[String, Json] implicit val decoder: JsonDecoder[Obj] = new JsonDecoder[Obj] { def unsafeDecode(trace: List[JsonError], in: RetractReader): Obj = diff --git a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala index b7379e496..6322cdb88 100644 --- a/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/ast/JsonSpec.scala @@ -13,36 +13,50 @@ object JsonSpec extends ZIOSpecDefault { suite("Json")( suite("apply")( test("Num()") { - assertTrue(Json.Num(0).toString == "0") && - assertTrue(Json.Num(0.0).toString == "0.0") && - assertTrue(Json.Num(1.0).toString == "1.0") && - assertTrue(Json.Num(-0.0).toString == "0.0") && - assertTrue(Json.Num(-1.0).toString == "-1.0") && - assertTrue(Json.Num(7: Byte).toString == "7") && - assertTrue(Json.Num(777: Short).toString == "777") && - assertTrue(Json.Num(123456789).toString == "123456789") && - assertTrue(Json.Num(1.2345678f).toString == "1.2345678") && - assertTrue(Json.Num(1.2345678901234567).toString == "1.2345678901234567") && - assertTrue(Json.Num(1234567890123456789L).toString == "1234567890123456789") && - assertTrue(Json.Num(BigInteger.valueOf(1234567890123456789L)).toString == "1234567890123456789") && - assertTrue(Json.Num(new BigInteger("12345678901234567890")).toString == "12345678901234567890") && - assertTrue(Json.Num(BigInt(1234567890123456789L)).toString == "1234567890123456789") && - assertTrue(Json.Num(BigInt("12345678901234567890")).toString == "12345678901234567890") && - assertTrue(Json.Num(BigDecimal(1234567890123456789L)).toString == "1234567890123456789") && - assertTrue(Json.Num(BigDecimal("12345678901234567890")).toString == "12345678901234567890") + assertTrue( + Json.Num(0).toString == "0", + Json.Num(0.0).toString == "0.0", + Json.Num(1.0).toString == "1.0", + Json.Num(-0.0).toString == "0.0", + Json.Num(-1.0).toString == "-1.0", + Json.Num(7: Byte).toString == "7", + Json.Num(777: Short).toString == "777", + Json.Num(123456789).toString == "123456789", + Json.Num(1.2345678f).toString == "1.2345678", + Json.Num(1.2345678901234567).toString == "1.2345678901234567", + Json.Num(1234567890123456789L).toString == "1234567890123456789", + Json.Num(BigInteger.valueOf(1234567890123456789L)).toString == "1234567890123456789", + Json.Num(new BigInteger("12345678901234567890")).toString == "12345678901234567890", + Json.Num(BigInt(1234567890123456789L)).toString == "1234567890123456789", + Json.Num(BigInt("12345678901234567890")).toString == "12345678901234567890", + Json.Num(BigDecimal(1234567890123456789L)).toString == "1234567890123456789", + Json.Num(BigDecimal("12345678901234567890")).toString == "12345678901234567890" + ) }, test("Bool()") { - assertTrue(Json.Bool.True eq Json.Bool(true)) && - assertTrue(Json.Bool.False eq Json.Bool(false)) + assertTrue( + Json.Bool.True eq Json.Bool(true), + Json.Bool.False eq Json.Bool(false) + ) }, test("()") { - assertTrue(Json.Obj.empty eq Json()) && - assertTrue(Json.Obj.empty eq Json.Obj()) && - assertTrue(Json.Arr.empty eq Json.Arr()) + assertTrue( + Json.Obj.empty eq Json(), + Json.Obj.empty eq Json.Obj(), + Json.Arr.empty eq Json.Arr() + ) }, test("(Chunk.empty)") { - assertTrue(Json.Obj.empty eq Json.Obj(Chunk.empty)) && - assertTrue(Json.Arr.empty eq Json.Arr(Chunk.empty)) + assertTrue( + Json.Obj.empty eq Json.Obj(Chunk.empty), + Json.Arr.empty eq Json.Arr(Chunk.empty) + ) + }, + test("Obj()") { + assertTrue( + Json.Obj("key", Json.Str("value")).toString == """{"key":"value"}""", + Json.Obj("key", Json.Str("value")) == Json.Obj("key" -> Json.Str("value")) + ) } ), suite("delete")( From 8768c60f6f47f68b2022b45ce1320e0008898cb5 Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Thu, 19 Jun 2025 16:36:02 +1000 Subject: [PATCH 265/311] Optimise `JsonCursor.element` and `JsonCursor.field` functions by avoiding to create a new `JsonCursor.FilterType` instance each time they're called (#1431) --- .../src/main/scala/zio/json/ast/JsonCursor.scala | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/ast/JsonCursor.scala b/zio-json/shared/src/main/scala/zio/json/ast/JsonCursor.scala index 7788a35c4..ba470025f 100644 --- a/zio-json/shared/src/main/scala/zio/json/ast/JsonCursor.scala +++ b/zio-json/shared/src/main/scala/zio/json/ast/JsonCursor.scala @@ -59,13 +59,6 @@ sealed trait JsonCursor[-From, +To <: Json] { self => } object JsonCursor { - def element(index: Int): JsonCursor[Json.Arr, Json] = DownElement(Identity.isArray, index) - - def field(name: String): JsonCursor[Json.Obj, Json] = DownField(Identity.isObject, name) - - def filter[A <: Json](jsonType: JsonType[A]): JsonCursor[Json, A] = - identity.filterType(jsonType) - val identity: JsonCursor[Json, Json] = Identity val isArray: JsonCursor[Json, Json.Arr] = filter(JsonType.Arr) @@ -80,12 +73,13 @@ object JsonCursor { val isString: JsonCursor[Json, Json.Str] = filter(JsonType.Str) - case object Identity extends JsonCursor[Json, Json] + def filter[A <: Json](jsonType: JsonType[A]): JsonCursor[Json, A] = identity.filterType(jsonType) + def element(index: Int): JsonCursor[Json.Arr, Json] = DownElement(isArray, index) + def field(name: String): JsonCursor[Json.Obj, Json] = DownField(isObject, name) + case object Identity extends JsonCursor[Json, Json] final case class DownField(parent: JsonCursor[_, Json.Obj], name: String) extends JsonCursor[Json.Obj, Json] - final case class DownElement(parent: JsonCursor[_, Json.Arr], index: Int) extends JsonCursor[Json.Arr, Json] - final case class FilterType[A <: Json](parent: JsonCursor[_, _ <: Json], jsonType: JsonType[A]) extends JsonCursor[Json, A] } From d10f008ffc3ee878f63e765f502be2c9b390e6f0 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:58:22 +0200 Subject: [PATCH 266/311] Update sbt, scripted-plugin to 1.11.3 (#1437) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index bbb0b608c..c02c575fd 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.2 +sbt.version=1.11.3 diff --git a/project/build.properties b/project/build.properties index bbb0b608c..c02c575fd 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.2 +sbt.version=1.11.3 From b03febe37526d09253278565e7fef39dfc3006cf Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:58:34 +0200 Subject: [PATCH 267/311] Update jsoniter-scala-core, ... to 2.36.7 (#1436) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 4c74b7c8c..31b1df11b 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.5" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.5" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.7" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.7" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From c429231c9c0c9bf50ff3def6fd1dbcbd80bbe913 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 06:11:19 +0200 Subject: [PATCH 268/311] Update cats-effect to 3.6.2 (#1438) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 31b1df11b..cd9f14e38 100644 --- a/build.sbt +++ b/build.sbt @@ -333,7 +333,7 @@ lazy val zioJsonInteropHttp4s = project libraryDependencies ++= Seq( "org.http4s" %% "http4s-dsl" % "0.23.30", "dev.zio" %% "zio" % zioVersion, - "org.typelevel" %% "cats-effect" % "3.6.1", + "org.typelevel" %% "cats-effect" % "3.6.2", "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % "test", "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" From a8a614ccc6728a55f9f9b3764e8aa5a1cd9e13e1 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Mon, 14 Jul 2025 06:24:38 +0200 Subject: [PATCH 269/311] Add `IArray` support (#1439) (#1440) --- .../scala-3/zio/json/JsonCodecVersionSpecific.scala | 8 ++++++-- .../scala-3/zio/json/JsonDecoderVersionSpecific.scala | 10 +++++++--- .../scala-3/zio/json/JsonEncoderVersionSpecific.scala | 10 +++++++--- .../shared/src/main/scala/zio/json/JsonDecoder.scala | 2 +- .../scala-3/zio/json/EncoderVesionSpecificSpec.scala | 6 ++++++ 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala index 0be993585..acfead58b 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonCodecVersionSpecific.scala @@ -3,8 +3,12 @@ package zio.json import scala.collection.immutable private[json] trait JsonCodecVersionSpecific { - inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonCodec[A] = DeriveJsonCodec.gen[A] - implicit def arraySeq[A: JsonEncoder: JsonDecoder: reflect.ClassTag]: JsonCodec[immutable.ArraySeq[A]] = JsonCodec(JsonEncoder.arraySeq[A], JsonDecoder.arraySeq[A]) + + inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonCodec[A] = DeriveJsonCodec.gen[A] + + implicit def iArray[A: JsonEncoder: JsonDecoder: reflect.ClassTag]: JsonCodec[IArray[A]] = + JsonCodec(JsonEncoder.iArray[A], JsonDecoder.iArray[A]) + } diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala index 4cb0edb13..0d92dae89 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonDecoderVersionSpecific.scala @@ -8,9 +8,6 @@ import scala.compiletime.* import scala.compiletime.ops.any.IsConst private[json] trait JsonDecoderVersionSpecific { - inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonDecoder[A] = - DeriveJsonDecoder.gen[A] - implicit def arraySeq[A: JsonDecoder: reflect.ClassTag]: JsonDecoder[immutable.ArraySeq[A]] = new CollectionJsonDecoder[immutable.ArraySeq[A]] { private[this] val arrayDecoder = JsonDecoder.array[A] @@ -20,6 +17,13 @@ private[json] trait JsonDecoderVersionSpecific { def unsafeDecode(trace: List[JsonError], in: RetractReader): immutable.ArraySeq[A] = immutable.ArraySeq.unsafeWrapArray(arrayDecoder.unsafeDecode(trace, in)) } + + inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonDecoder[A] = + DeriveJsonDecoder.gen[A] + + implicit def iArray[A](implicit A: JsonDecoder[A], classTag: reflect.ClassTag[A]): JsonDecoder[IArray[A]] = + JsonDecoder.array[A].map(IArray.unsafeFromArray) + } trait DecoderLowPriorityVersionSpecific { diff --git a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala index e9b290068..67de4b045 100644 --- a/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala +++ b/zio-json/shared/src/main/scala-3/zio/json/JsonEncoderVersionSpecific.scala @@ -7,9 +7,6 @@ import scala.collection.immutable import scala.compiletime.ops.any.IsConst private[json] trait JsonEncoderVersionSpecific { - inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonEncoder[A] = - DeriveJsonEncoder.gen[A] - implicit def arraySeq[A: JsonEncoder: scala.reflect.ClassTag]: JsonEncoder[immutable.ArraySeq[A]] = new JsonEncoder[immutable.ArraySeq[A]] { private[this] val arrayEnc = JsonEncoder.array[A] @@ -22,6 +19,13 @@ private[json] trait JsonEncoderVersionSpecific { override final def toJsonAST(as: immutable.ArraySeq[A]): Either[String, Json] = arrayEnc.toJsonAST(as.unsafeArray.asInstanceOf[Array[A]]) } + + inline def derived[A: deriving.Mirror.Of](using config: JsonCodecConfiguration): JsonEncoder[A] = + DeriveJsonEncoder.gen[A] + + implicit def iArray[A](implicit A: JsonEncoder[A], classTag: scala.reflect.ClassTag[A]): JsonEncoder[IArray[A]] = + JsonEncoder.array[A].contramap[IArray[A]](arr => IArray.genericWrapArray(arr).toArray) + } private[json] trait EncoderLowPriorityVersionSpecific { diff --git a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala index 1b3da62ed..f10c2cdb0 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala @@ -97,7 +97,7 @@ trait JsonDecoder[A] extends JsonDecoderPlatformSpecific[A] { } /** - * Returns this decoder but widened to the its given super-type + * Returns this decoder but widened to the given super-type */ final def widen[B >: A]: JsonDecoder[B] = self.asInstanceOf[JsonDecoder[B]] diff --git a/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala b/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala index 2cad2a16c..ab59ea3e0 100644 --- a/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala +++ b/zio-json/shared/src/test/scala-3/zio/json/EncoderVesionSpecificSpec.scala @@ -17,6 +17,12 @@ object EncoderVesionSpecificSpec extends ZIOSpecDefault { assert(immutable.ArraySeq[String]().toJsonPretty)(equalTo("[]")) && assert(immutable.ArraySeq("foo", "bar").toJsonPretty)(equalTo("[\n \"foo\",\n \"bar\"\n]")) }, + test("IArray") { + assert(IArray.empty[Int].toJson)(equalTo("[]")) && + assert(IArray(1, 2, 3).toJson)(equalTo("[1,2,3]")) && + assert(IArray.empty[String].toJsonPretty)(equalTo("[]")) && + assert(IArray("foo", "bar").toJsonPretty)(equalTo("[\n \"foo\",\n \"bar\"\n]")) + }, test("Derives for a product type") { case class Foo(bar: String) derives JsonEncoder From 2d7bd4b6038135d58a1591bec4b4acdc662ca030 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 04:49:45 +0200 Subject: [PATCH 270/311] Update snakeyaml-engine to 2.10 (#1441) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index c991f6c51..adc1084c9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -13,4 +13,4 @@ addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.32") -libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.9" +libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.10" From 8cf441a22fafb52b13da9bb2b2b4ed9187169ab8 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:27:53 +0200 Subject: [PATCH 271/311] Update zio, zio-streams, zio-test, ... to 2.1.20 (#1442) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index cd9f14e38..c375a2f3f 100644 --- a/build.sbt +++ b/build.sbt @@ -58,7 +58,7 @@ addCommandAlias( "zioJsonMacrosNative/test" ) -val zioVersion = "2.1.19" +val zioVersion = "2.1.20" lazy val zioJsonRoot = project .in(file(".")) From cfd0126c0f765e52b8594e9a4fac7322b0e579d8 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 06:53:54 +0200 Subject: [PATCH 272/311] Update jsoniter-scala-core, ... to 2.37.0 (#1444) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index c375a2f3f..ee60fa4a8 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.36.7" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.36.7" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.0" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From dfe8065eb3879d9d6787f84d436bb2089d5096f6 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:43:49 +1000 Subject: [PATCH 273/311] Update cats-effect to 3.6.3 (#1443) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index ee60fa4a8..409db0523 100644 --- a/build.sbt +++ b/build.sbt @@ -333,7 +333,7 @@ lazy val zioJsonInteropHttp4s = project libraryDependencies ++= Seq( "org.http4s" %% "http4s-dsl" % "0.23.30", "dev.zio" %% "zio" % zioVersion, - "org.typelevel" %% "cats-effect" % "3.6.2", + "org.typelevel" %% "cats-effect" % "3.6.3", "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % "test", "dev.zio" %% "zio-test" % zioVersion % "test", "dev.zio" %% "zio-test-sbt" % zioVersion % "test" From 9a3599b9a60578df9276d93e9e58e59233b179b1 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:44:02 +1000 Subject: [PATCH 274/311] Update sbt, scripted-plugin to 1.11.4 (#1446) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index c02c575fd..489e0a72d 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.3 +sbt.version=1.11.4 diff --git a/project/build.properties b/project/build.properties index c02c575fd..489e0a72d 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.3 +sbt.version=1.11.4 From 4fa762403a559b0bac278c21c33eb51708514f1f Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:44:14 +1000 Subject: [PATCH 275/311] Update jsoniter-scala-core, ... to 2.37.1 (#1445) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 409db0523..87314ec86 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.0" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.1" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.1" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 5bd099263b8adcf3170b1e6d7349d7a97f5be959 Mon Sep 17 00:00:00 2001 From: scarf Date: Thu, 7 Aug 2025 10:40:53 +0900 Subject: [PATCH 276/311] docs: add tabs for scala 3 (#1201) * docs: add docs for scala 3 * docs: use tabs * Apply suggestions from code review Co-authored-by: Jules Ivanic * Update README.md Co-authored-by: Jules Ivanic --------- Co-authored-by: Jules Ivanic --- README.md | 80 +++++++++++++++++++----- docs/{index.md => index.mdx} | 118 +++++++++++++++++++++++++++++++++-- 2 files changed, 176 insertions(+), 22 deletions(-) rename docs/{index.md => index.mdx} (71%) diff --git a/README.md b/README.md index b5e5faa71..8677b58a7 100644 --- a/README.md +++ b/README.md @@ -47,17 +47,25 @@ Say we want to be able to read some JSON like into a Scala `case class` ```scala -case class Banana(curvature: Double) +final case class Banana(curvature: Double) ``` -To do this, we create an *instance* of the `JsonDecoder` typeclass for `Banana` using the `zio-json` code generator. It is best practice to put it on the companion of `Banana`, like so +To do this, we derive an *instance* of the `JsonDecoder` typeclass for `Banana`. ```scala -object Banana { - implicit val decoder: JsonDecoder[Banana] = DeriveJsonDecoder.gen[Banana] -} +final case class Banana(curvature: Double) derives JsonDecoder ``` +> [!NOTE] +> +> In scala 2, we need to use the `zio-json` semi-automatic derivation. It is best practice to put it on the companion of `Banana`, like so +> +> ```scala +> object Banana { +> implicit val decoder: JsonDecoder[Banana] = DeriveJsonDecoder.gen[Banana] +> } +> ``` + Now we can parse JSON into our object ``` @@ -65,13 +73,10 @@ scala> """{"curvature":0.5}""".fromJson[Banana] val res: Either[String, Banana] = Right(Banana(0.5)) ``` -Likewise, to produce JSON from our data we define a `JsonEncoder` +Likewise, to produce JSON from our data we derive a `JsonEncoder` ```scala -object Banana { - ... - implicit val encoder: JsonEncoder[Banana] = DeriveJsonEncoder.gen[Banana] -} +final case class Banana(curvature: Double) derives JsonEncoder scala> Banana(0.5).toJson val res: String = {"curvature":0.5} @@ -83,6 +88,16 @@ val res: String = } ``` +> [!NOTE] +> +> In scala 2: +> ```scala +> object Banana { +> ... +> implicit val encoder: JsonEncoder[Banana] = DeriveJsonEncoder.gen[Banana] +> } +> ``` + And bad JSON will produce an error in `jq` syntax with an additional piece of contextual information (in parentheses) ``` @@ -93,20 +108,51 @@ val res: Either[String, Banana] = Left(.curvature(expected a Double)) Say we extend our data model to include more data types ```scala -sealed trait Fruit -case class Banana(curvature: Double) extends Fruit -case class Apple (poison: Boolean) extends Fruit +enum Fruit { + case Banana(curvature: Double) + case Apple(poison: Boolean) +} ``` -we can generate the encoder and decoder for the entire `sealed` family +we can generate the encoder and decoder for the entire `sealed` family using `JsonCodec` ```scala -object Fruit { - implicit val decoder: JsonDecoder[Fruit] = DeriveJsonDecoder.gen[Fruit] - implicit val encoder: JsonEncoder[Fruit] = DeriveJsonEncoder.gen[Fruit] +enum Fruit derives JsonCodec { + case Banana(curvature: Double) + case Apple(poison: Boolean) } ``` +> [!NOTE] +> +> In scala 2: +> +> ```scala mdoc:compile-only +> import zio.json._ +> +> sealed trait Fruit +> final case class Banana(curvature: Double) extends Fruit +> final case class Apple(poison: Boolean) extends Fruit +> +> object Fruit { +> implicit val decoder: JsonDecoder[Fruit] = +> DeriveJsonDecoder.gen[Fruit] +> +> implicit val encoder: JsonEncoder[Fruit] = +> DeriveJsonEncoder.gen[Fruit] +> } +> +> val json1 = """{ "Banana":{ "curvature":0.5 }}""" +> val json2 = """{ "Apple": { "poison": false }}""" +> val malformedJson = """{ "Banana":{ "curvature": true }}""" +> +> json1.fromJson[Fruit] +> json2.fromJson[Fruit] +> malformedJson.fromJson[Fruit] +> +> List(Apple(false), Banana(0.4)).toJsonPretty +> ``` + allowing us to load the fruit based on a single field type tag in the JSON ``` diff --git a/docs/index.md b/docs/index.mdx similarity index 71% rename from docs/index.md rename to docs/index.mdx index e5819369d..45d54619d 100644 --- a/docs/index.md +++ b/docs/index.mdx @@ -4,6 +4,9 @@ title: "Getting Started with ZIO Json" sidebar_label: "Getting Started" --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + [ZIO Json](https://github.com/zio/zio-json) is a fast and secure JSON library with tight ZIO integration. @PROJECT_BADGES@ @@ -42,10 +45,23 @@ Let's try a simple example of encoding and decoding JSON using ZIO JSON. All the following code snippets assume that the following imports have been declared + + + ```scala import zio.json._ ``` + + + +```scala +import zio.json.* +``` + + + + Say we want to be able to read some JSON like ```json @@ -55,9 +71,12 @@ Say we want to be able to read some JSON like into a Scala `case class` ```scala -case class Banana(curvature: Double) +final case class Banana(curvature: Double) ``` + + + To do this, we create an *instance* of the `JsonDecoder` typeclass for `Banana` using the `zio-json` code generator. It is best practice to put it on the companion of `Banana`, like so ```scala @@ -66,7 +85,19 @@ object Banana { } ``` -_Note: If you’re using Scala 3 and your case class is defining default parameters, `-Yretain-trees` needs to be added to `scalacOptions`._ + + + +To do this, we derive an *instance* of the `JsonDecoder` typeclass for `Banana`. + +```scala +final case class Banana(curvature: Double) derives JsonDecoder +``` + +Note: If your case class is defining default parameters, -Yretain-trees needs to be added to scalacOptions. + + + Now we can parse JSON into our object @@ -75,14 +106,29 @@ scala> """{"curvature":0.5}""".fromJson[Banana] val res: Either[String, Banana] = Right(Banana(0.5)) ``` -Likewise, to produce JSON from our data we define a `JsonEncoder` +Likewise, to produce JSON from our data we derive a `JsonEncoder` + + + ```scala object Banana { ... implicit val encoder: JsonEncoder[Banana] = DeriveJsonEncoder.gen[Banana] } +``` + + + + +```scala +final case class Banana(curvature: Double) derives JsonEncoder +``` + + + +``` scala> Banana(0.5).toJson val res: String = {"curvature":0.5} @@ -102,14 +148,33 @@ val res: Either[String, Banana] = Left(.curvature(expected a Double)) Say we extend our data model to include more data types + + + ```scala sealed trait Fruit -case class Banana(curvature: Double) extends Fruit -case class Apple (poison: Boolean) extends Fruit +final case class Banana(curvature: Double) extends Fruit +final case class Apple (poison: Boolean) extends Fruit +``` + + + + +```scala +enum Fruit { + case Banana(curvature: Double) + case Apple (poison: Boolean) +} ``` + + + we can generate the encoder and decoder for the entire `sealed` family + + + ```scala object Fruit { implicit val decoder: JsonDecoder[Fruit] = DeriveJsonDecoder.gen[Fruit] @@ -117,6 +182,19 @@ object Fruit { } ``` + + + +```scala +enum Fruit derives JsonCodec { + case Banana(curvature: Double) + case Apple (poison: Boolean) +} +``` + + + + allowing us to load the fruit based on a single field type tag in the JSON ``` @@ -129,6 +207,9 @@ val res: Either[String, Fruit] = Right(Apple(false)) Almost all of the standard library data types are supported as fields on the case class, and it is easy to add support if one is missing. + + + ```scala mdoc:compile-only import zio.json._ @@ -155,6 +236,33 @@ malformedJson.fromJson[Fruit] List(Apple(false), Banana(0.4)).toJsonPretty ``` + + + +```scala +import zio.json.* + +enum Fruit derives JsonCodec { + case Banana(curvature: Double) + case Apple(poison: Boolean) +} + +export Fruit.* + +val json1 = """{ "Banana":{ "curvature":0.5 }}""" +val json2 = """{ "Apple": { "poison": false }}""" +val malformedJson = """{ "Banana":{ "curvature": true }}""" + +json1.fromJson[Fruit] +json2.fromJson[Fruit] +malformedJson.fromJson[Fruit] + +List(Apple(false), Banana(0.4)).toJsonPretty +``` + + + + # How Extreme **performance** is achieved by decoding JSON directly from the input source into business objects (inspired by [plokhotnyuk](https://github.com/plokhotnyuk/jsoniter-scala)). Although not a requirement, the latest advances in [Java Loom](https://wiki.openjdk.java.net/display/loom/Main) can be used to support arbitrarily large payloads with near-zero overhead. From 0350e918362e8c88371403b797a9d2d95bb0ce52 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 06:23:22 +0200 Subject: [PATCH 277/311] Update jsoniter-scala-core, ... to 2.37.2 (#1447) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 87314ec86..c252e950b 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.1" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.1" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.2" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From b7432962d9917eed4026c4e30b2a185fdfca4a11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 09:45:16 +1000 Subject: [PATCH 278/311] Bump actions/checkout from 4.2.2 to 5.0.0 (#1448) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.2.2...v5.0.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/site.yml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b1b46907..8520a07af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout current branch - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Setup Action @@ -40,7 +40,7 @@ jobs: scala: ['2.13.16', '3.3.6'] steps: - name: Checkout current branch - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Setup Action @@ -58,7 +58,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout current branch - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Setup Action uses: coursier/setup-action@v1 with: @@ -80,7 +80,7 @@ jobs: platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Install Boehm GC @@ -107,7 +107,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout current branch - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: fetch-depth: 300 - name: Fetch tags @@ -133,7 +133,7 @@ jobs: if: ${{ github.event_name != 'pull_request' }} steps: - name: Checkout current branch - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: fetch-depth: 0 - name: Setup Action diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index c15b6da04..d6303164e 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -18,7 +18,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} steps: - name: Git Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: fetch-depth: '0' - name: Setup Action @@ -36,7 +36,7 @@ jobs: if: ${{ ((github.event_name == 'release') && (github.event.action == 'published')) || (github.event_name == 'workflow_dispatch') }} steps: - name: Git Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: fetch-depth: '0' - name: Setup Action @@ -59,7 +59,7 @@ jobs: if: ${{ (github.event_name == 'push') || ((github.event_name == 'release') && (github.event.action == 'published')) }} steps: - name: Git Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: ref: ${{ github.head_ref }} fetch-depth: '0' From 3b0e50a37fda1769b56ac5e5df601fcd50fe1d2f Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:43:51 +1000 Subject: [PATCH 279/311] Update jsoniter-scala-core, ... to 2.37.4 (#1449) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index c252e950b..a95193fe6 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.2" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.4" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.4" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From d7d1b2456576b766b8c8622e04398b4ca278a600 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 06:31:28 +0200 Subject: [PATCH 280/311] Update jsoniter-scala-core, ... to 2.37.5 (#1450) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index a95193fe6..2acb0a7cf 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.4" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.4" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.5" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.5" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 723e3ff545962ec3cda6a3e05250a24e87841cfd Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Sun, 17 Aug 2025 08:06:45 +1000 Subject: [PATCH 281/311] Update jsoniter-scala-core, ... to 2.37.6 (#1451) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 2acb0a7cf..7b9c5da2b 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.5" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.5" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.6" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.6" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From e27851076bb1fc4a2c37d3b97b17d3a428f8aeb8 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 12:50:34 +1000 Subject: [PATCH 282/311] Update sbt-ci-release to 1.11.2 (#1452) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index adc1084c9..ccce37417 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.1") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") From 2deceeab0c62064cdfc7833e0b00df1adf50c5d2 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:09:27 +1000 Subject: [PATCH 283/311] Update zio-sbt-website to 0.4.0-alpha.33 (#1453) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index ccce37417..cf565f05c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -11,6 +11,6 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.8") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") -addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.32") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.33") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.10" From 9b964dd3e5286195a82b7c4576714645aa67abb4 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:54:56 +1000 Subject: [PATCH 284/311] Update sbt, scripted-plugin to 1.11.5 (#1454) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index 489e0a72d..e480c675f 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.4 +sbt.version=1.11.5 diff --git a/project/build.properties b/project/build.properties index 489e0a72d..e480c675f 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.4 +sbt.version=1.11.5 From 163146c70074aad1df5288afe0d0545df1c50ae8 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:44:36 +1000 Subject: [PATCH 285/311] Update zio-sbt-website to 0.4.0-alpha.34 (#1455) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index cf565f05c..63c0542d8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -11,6 +11,6 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.8") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") -addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.33") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.34") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.10" From f25ba0273639e8c53bb71a614fab5077dba5f1db Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 06:59:40 +0200 Subject: [PATCH 286/311] Update snakeyaml to 2.5 (#1456) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 7b9c5da2b..5f643daf7 100644 --- a/build.sbt +++ b/build.sbt @@ -290,7 +290,7 @@ lazy val zioJsonYaml = project .settings(buildInfoSettings("zio.json.yaml")) .settings( libraryDependencies ++= Seq( - "org.yaml" % "snakeyaml" % "2.4", + "org.yaml" % "snakeyaml" % "2.5", "org.scala-lang.modules" %% "scala-collection-compat" % "2.13.0", "dev.zio" %% "zio" % zioVersion, "dev.zio" %% "zio-test" % zioVersion % "test", From 4c6fe33e40c5455e40902a9ea5208fd92b5f589f Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 12:47:36 +1000 Subject: [PATCH 287/311] Update zio, zio-streams, zio-test, ... to 2.1.21 (#1458) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5f643daf7..58a8c08ec 100644 --- a/build.sbt +++ b/build.sbt @@ -58,7 +58,7 @@ addCommandAlias( "zioJsonMacrosNative/test" ) -val zioVersion = "2.1.20" +val zioVersion = "2.1.21" lazy val zioJsonRoot = project .in(file(".")) From 47cb32ea16bef03d5b3681c4aac6f4b5d9cd9c7b Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 12:47:43 +1000 Subject: [PATCH 288/311] Update jsoniter-scala-core, ... to 2.37.8 (#1457) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 58a8c08ec..3e281ca1f 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.6" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.6" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.8" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.8" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 5dd4273f9b36beb0c305d1b379fbd1821b9843fd Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 06:50:40 +0200 Subject: [PATCH 289/311] Update sbt-scalajs, scalajs-compiler, ... to 1.20.1 (#1459) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 63c0542d8..7e3b6ab85 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -6,7 +6,7 @@ addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.8") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") From 72b501a37c0eca6d55b635e17308a0f6f29a4425 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 06:50:52 +0200 Subject: [PATCH 290/311] Update sbt, scripted-plugin to 1.11.6 (#1460) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index e480c675f..5e6884d37 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.5 +sbt.version=1.11.6 diff --git a/project/build.properties b/project/build.properties index e480c675f..5e6884d37 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.5 +sbt.version=1.11.6 From 496a21f43324f47d0dcb84b5539f451f9dbc140a Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:59:59 +1000 Subject: [PATCH 291/311] Update jsoniter-scala-core, ... to 2.37.9 (#1461) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 3e281ca1f..2ab8a93a1 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.8" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.8" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.9" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.9" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 3067527961e186224ce211a4df0588053569933f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 18:36:37 +0200 Subject: [PATCH 292/311] Bump actions/setup-node from 4 to 5 (#1462) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/site.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index d6303164e..cfe207784 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -45,7 +45,7 @@ jobs: jvm: temurin:17 apps: sbt - name: Setup NodeJs - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 16.x registry-url: https://registry.npmjs.org From 99d3b901788432ac3591480bb2dc85af60212af5 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:21:45 +1000 Subject: [PATCH 293/311] Update jsoniter-scala-core, ... to 2.37.10 (#1463) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 2ab8a93a1..50a6ec30b 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.9" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.9" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.10" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.10" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From c634bf61b517cdae0a595ba449bdba763beca67d Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 09:28:15 +0200 Subject: [PATCH 294/311] Update sbt-header to 5.11.0 (#1464) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 7e3b6ab85..becf63069 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,7 +3,7 @@ addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") +addSbtPlugin("com.github.sbt" % "sbt-header" % "5.11.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") From 6f78d7f587d1651692c1bec44b073e5e7e21d124 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:36:04 +0400 Subject: [PATCH 295/311] Update jsoniter-scala-core, ... to 2.37.11 (#1465) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 50a6ec30b..b74563c93 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.10" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.10" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.11" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.11" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From f0da007b20466442ba903c32558a5ba82ae49c95 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 06:43:46 +0200 Subject: [PATCH 296/311] Update jsoniter-scala-core, ... to 2.38.0 (#1466) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index b74563c93..d3e0065f7 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.37.11" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.37.11" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.38.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.38.0" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 8a4e874771b9e68d76b34d63cce70a1992f57d25 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:00:29 +0400 Subject: [PATCH 297/311] Update http4s-dsl to 0.23.31 (#1468) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d3e0065f7..37fe3871c 100644 --- a/build.sbt +++ b/build.sbt @@ -331,7 +331,7 @@ lazy val zioJsonInteropHttp4s = project .settings(buildInfoSettings("zio.json.interop.http4s")) .settings( libraryDependencies ++= Seq( - "org.http4s" %% "http4s-dsl" % "0.23.30", + "org.http4s" %% "http4s-dsl" % "0.23.31", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.6.3", "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % "test", From eb828011a3f00d61275974820a9a78c3c85e4721 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:02:55 +0400 Subject: [PATCH 298/311] Update zio-sbt-website to 0.4.0-alpha.35 (#1467) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index becf63069..3529f629e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -11,6 +11,6 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.8") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") -addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.34") +addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.35") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.10" From 6a2bf3887568674402ac3866196bf41f4d554e02 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 07:02:01 +0400 Subject: [PATCH 299/311] Update jsoniter-scala-core, ... to 2.38.2 (#1471) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 37fe3871c..6a941ce9d 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.38.0" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.38.0" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.38.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.38.2" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 0e1200815f400231d64491f5382d2ee840aed325 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 08:07:24 +0400 Subject: [PATCH 300/311] Update http4s-dsl to 0.23.32 (#1470) --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 6a941ce9d..4c500b1b2 100644 --- a/build.sbt +++ b/build.sbt @@ -331,7 +331,7 @@ lazy val zioJsonInteropHttp4s = project .settings(buildInfoSettings("zio.json.interop.http4s")) .settings( libraryDependencies ++= Seq( - "org.http4s" %% "http4s-dsl" % "0.23.31", + "org.http4s" %% "http4s-dsl" % "0.23.32", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.6.3", "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % "test", From 230bb672ec3345e3e00ff39238c59c9ac9c2940b Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 06:22:06 +0200 Subject: [PATCH 301/311] Update kind-projector to 0.13.4 (#1473) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/BuildHelper.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index ea5619a93..ce2569423 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -238,7 +238,7 @@ object BuildHelper { Seq( "com.github.ghik" % "silencer-lib" % SilencerVersion % Provided cross CrossVersion.full, compilerPlugin("com.github.ghik" % "silencer-plugin" % SilencerVersion cross CrossVersion.full), - compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.3" cross CrossVersion.full) + compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.4" cross CrossVersion.full) ) }, versionScheme := Some("early-semver"), From fafd2bbbe734fa0a4d2ef17db481bb32eeaaec2a Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 06:22:16 +0200 Subject: [PATCH 302/311] Update circe-core, circe-generic, ... to 0.14.15 (#1472) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 4c500b1b2..de5553521 100644 --- a/build.sbt +++ b/build.sbt @@ -86,7 +86,7 @@ lazy val zioJsonRoot = project zioJsonGolden ) -val circeVersion = "0.14.14" +val circeVersion = "0.14.15" lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("zio-json")) From 1bec2e643074139c9a82f70bb1b0446fa285239d Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 07:26:01 +0400 Subject: [PATCH 303/311] Update jsoniter-scala-core, ... to 2.38.3 (#1474) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index de5553521..af22a1678 100644 --- a/build.sbt +++ b/build.sbt @@ -112,8 +112,8 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", "dev.zio" %%% "zio-test" % zioVersion % "test", "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.38.2" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.38.2" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.38.3" % "test", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.38.3" % "test", "io.circe" %%% "circe-core" % circeVersion % "test", "io.circe" %%% "circe-generic" % circeVersion % "test", "io.circe" %%% "circe-parser" % circeVersion % "test", From 56f432bd5319b50054ec6edfc3935395fdbb53e7 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:14:46 +0400 Subject: [PATCH 304/311] Update sbt, scripted-plugin to 1.11.7 (#1475) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- examples/zio-json-golden/project/build.properties | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/zio-json-golden/project/build.properties b/examples/zio-json-golden/project/build.properties index 5e6884d37..01a16ed14 100644 --- a/examples/zio-json-golden/project/build.properties +++ b/examples/zio-json-golden/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.6 +sbt.version=1.11.7 diff --git a/project/build.properties b/project/build.properties index 5e6884d37..01a16ed14 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.6 +sbt.version=1.11.7 From 599ed5449959e95c637219f8b2650ec0ebd4170d Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 07:58:59 +0200 Subject: [PATCH 305/311] Update auxlib, clib, javalib, nativelib, ... to 0.5.9 (#1478) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 3529f629e..26f0accd4 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,7 +7,7 @@ addSbtPlugin("com.github.sbt" % "sbt-header" % "5.11.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.8") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.9") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") From c84917b75d84efaefa90f2985e8ef7dbe8c01d0e Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:31:12 +0400 Subject: [PATCH 306/311] Update scala3-library, ... to 3.3.7 (#1479) --- .github/workflows/ci.yml | 4 ++-- project/BuildHelper.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8520a07af..a24a4b72a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: fail-fast: false matrix: java: ['11', '21'] - scala: ['2.13.16', '3.3.6'] + scala: ['2.13.16', '3.3.7'] steps: - name: Checkout current branch uses: actions/checkout@v5.0.0 @@ -76,7 +76,7 @@ jobs: fail-fast: false matrix: java: ['11', '21'] - scala: ['2.12.20', '2.13.16', '3.3.6'] + scala: ['2.12.20', '2.13.16', '3.3.7'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index ce2569423..de8ec77f7 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -28,7 +28,7 @@ object BuildHelper { } val Scala212: String = versions("2.12") val Scala213: String = versions("2.13") - val ScalaDotty: String = "3.3.6" + val ScalaDotty: String = "3.3.7" val SilencerVersion = "1.7.19" From dc960e4be2b592e87ddd0190f2b1a2d0fcd7cb35 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 07:40:56 +0200 Subject: [PATCH 307/311] Update zio, zio-streams, zio-test, ... to 2.1.22 (#1480) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index af22a1678..5a0c10042 100644 --- a/build.sbt +++ b/build.sbt @@ -58,7 +58,7 @@ addCommandAlias( "zioJsonMacrosNative/test" ) -val zioVersion = "2.1.21" +val zioVersion = "2.1.22" lazy val zioJsonRoot = project .in(file(".")) From f6cebbdc01f08fa01b096d3fd92a8866df64b436 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 19:53:29 +0200 Subject: [PATCH 308/311] Bump actions/setup-node from 5 to 6 (#1481) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/site.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index cfe207784..a0e7cc2aa 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -45,7 +45,7 @@ jobs: jvm: temurin:17 apps: sbt - name: Setup NodeJs - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: 16.x registry-url: https://registry.npmjs.org From f370600d0c13f808fd619af151a314cdbd99a888 Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 08:25:23 +0200 Subject: [PATCH 309/311] Update sbt-scoverage to 2.4.0 (#1483) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 26f0accd4..47f7e45b2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -10,7 +10,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.9") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.0") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.35") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.10" From a36fea8a98a74e221ca1c9f83b1825e362d288ee Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:56:37 +1100 Subject: [PATCH 310/311] Update http4s-dsl to 0.23.33 (#1484) Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5a0c10042..be2060d1a 100644 --- a/build.sbt +++ b/build.sbt @@ -331,7 +331,7 @@ lazy val zioJsonInteropHttp4s = project .settings(buildInfoSettings("zio.json.interop.http4s")) .settings( libraryDependencies ++= Seq( - "org.http4s" %% "http4s-dsl" % "0.23.32", + "org.http4s" %% "http4s-dsl" % "0.23.33", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.6.3", "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % "test", From bd1b40fb9cca051c66619da817e77f605e563f3f Mon Sep 17 00:00:00 2001 From: "zio-scala-steward[bot]" <145262613+zio-scala-steward[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:22:33 +1100 Subject: [PATCH 311/311] Update scala-library, scala-reflect to 2.13.17 (#1477) * Update scala-library, scala-reflect to 2.13.17 * Update sbt and CI stuff to make the code compile again * Try fix lib cross-compat issue * clean * Fix `zioJsonInteropScalaz7x` * Try fix zioJsonInteropScalaz7x * Try fix zioJsonInteropScalaz7x * Fix OOMs in CI --------- Co-authored-by: zio-scala-steward[bot] <145262613+zio-scala-steward[bot]@users.noreply.github.com> Co-authored-by: Jules Ivanic --- .github/workflows/ci.yml | 18 ++--- build.sbt | 79 ++++++++++--------- examples/zio-json-golden/build.sbt | 2 +- project/BuildHelper.scala | 77 ++++++------------ project/build.properties | 2 +- project/plugins.sbt | 1 + .../scala/zio/json/data/geojson/GeoJSON.scala | 9 +-- .../zio/json/data/googlemaps/GoogleMaps.scala | 8 +- .../scala/zio/json/data/twitter/Twitter.scala | 15 ++-- 9 files changed, 91 insertions(+), 120 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a24a4b72a..0bf03ed7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,8 @@ name: CI env: - JDK_JAVA_OPTIONS: -XX:+PrintCommandLineFlags # JDK_JAVA_OPTIONS is _the_ env. variable to use for modern Java - JVM_OPTS: -XX:+PrintCommandLineFlags # for Java 8 only (sadly, it is not modern enough for JDK_JAVA_OPTIONS) + JDK_JAVA_OPTIONS: -XX:+PrintCommandLineFlags -Xms2G -Xmx8G -Xss4M -XX:+UseG1GC -XX:ReservedCodeCacheSize=512M -XX:NonProfiledCodeHeapSize=256M # JDK_JAVA_OPTIONS is _the_ env. variable to use for modern Java + SBT_OPTS: -XX:+PrintCommandLineFlags -Xms2G -Xmx8G -Xss4M -XX:+UseG1GC -XX:ReservedCodeCacheSize=512M -XX:NonProfiledCodeHeapSize=256M # Needed for sbt on: pull_request: @@ -29,7 +29,7 @@ jobs: - name: Cache scala dependencies uses: coursier/cache-action@v6 - name: Lint code - run: sbt "++2.12; check; ++2.13; check; ++3.3; check" + run: sbt "++2.12.x; check; ++2.13.x; check; ++3.3.x; check" benchmarks: runs-on: ubuntu-22.04 @@ -37,7 +37,7 @@ jobs: fail-fast: false matrix: java: ['11', '21'] - scala: ['2.13.16', '3.3.7'] + scala: ['2.13.x', '3.x'] steps: - name: Checkout current branch uses: actions/checkout@v5.0.0 @@ -51,7 +51,7 @@ jobs: - name: Cache scala dependencies uses: coursier/cache-action@v6 - name: Compile benchmarks - run: sbt ++${{ matrix.scala }}! jmh:compile + run: sbt ++${{ matrix.scala }} jmh:compile mdoc: runs-on: ubuntu-22.04 @@ -76,7 +76,7 @@ jobs: fail-fast: false matrix: java: ['11', '21'] - scala: ['2.12.20', '2.13.16', '3.3.7'] + scala: ['2.12.x', '2.13.x', '3.x'] platform: ['JVM', 'JS', 'Native'] steps: - name: Checkout current branch @@ -97,10 +97,10 @@ jobs: if: matrix.platform == 'Native' run: sudo apt-get update && sudo apt-get install -y libuv1-dev - name: Run Macros tests - if: ${{ !startsWith(matrix.scala, '3.3.') }} - run: sbt ++${{ matrix.scala }}! testScala2${{ matrix.platform }} + if: ${{ !startsWith(matrix.scala, '3.') }} + run: sbt ++${{ matrix.scala }} testScala2${{ matrix.platform }} - name: Run tests - run: sbt ++${{ matrix.scala }}! test${{ matrix.platform }} + run: sbt ++${{ matrix.scala }} test${{ matrix.platform }} mima_check: runs-on: ubuntu-22.04 diff --git a/build.sbt b/build.sbt index be2060d1a..70adf24c1 100644 --- a/build.sbt +++ b/build.sbt @@ -5,10 +5,11 @@ import com.typesafe.tools.mima.plugin.MimaKeys.mimaPreviousArtifacts import explicitdeps.ExplicitDepsPlugin.autoImport.moduleFilterRemoveValue import sbtcrossproject.CrossPlugin.autoImport.crossProject -Global / onChangedBuildSource := IgnoreSourceChanges +Global / onChangedBuildSource := ReloadOnSourceChanges inThisBuild( List( + scalaVersion := Scala213, organization := "dev.zio", homepage := Some(url("https://zio.dev/zio-json/")), licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), @@ -19,7 +20,12 @@ inThisBuild( "john@degoes.net", url("http://degoes.net") ) - ) + ), + publishTo := { + val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/" + if (isSnapshot.value) Some("central-snapshots" at centralSnapshots) + else localStaging.value + } ) ) @@ -30,32 +36,32 @@ addCommandAlias("prepare", "fmt") addCommandAlias( "testJVM", - "zioJsonJVM/test; zioJsonYaml/test; zioJsonInteropHttp4s/test; zioJsonInteropScalaz7xJVM/test; zioJsonGolden/test; zioJsonInteropRefinedJVM/test" + "zioJsonJVM/test; zioJsonYaml/test; zioJsonInteropHttp4s/test; zioJsonGolden/test; zioJsonInteropRefinedJVM/test" ) addCommandAlias( "testJS", - "zioJsonJS/test; zioJsonInteropScalaz7xJS/test; zioJsonInteropRefinedJS/test" + "zioJsonJS/test; zioJsonInteropRefinedJS/test" ) addCommandAlias( "testNative", - "zioJsonNative/test; zioJsonInteropScalaz7xNative/test; zioJsonInteropRefinedNative/test" + "zioJsonNative/test; zioJsonInteropRefinedNative/test" ) addCommandAlias( "testScala2JVM", - "zioJsonMacrosJVM/test" + "zioJsonMacrosJVM/test; zioJsonInteropScalaz7xJVM/test" ) addCommandAlias( "testScala2JS", - "zioJsonMacrosJS/test" + "zioJsonMacrosJS/test; zioJsonInteropScalaz7xJS/test" ) addCommandAlias( "testScala2Native", - "zioJsonMacrosNative/test" + "zioJsonMacrosNative/test; zioJsonInteropScalaz7xNative/test" ) val zioVersion = "2.1.22" @@ -65,7 +71,8 @@ lazy val zioJsonRoot = project .settings( publish / skip := true, mimaPreviousArtifacts := Set(), - unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library") + unusedCompileDependenciesFilter -= moduleFilter("org.scala-js", "scalajs-library"), + crossScalaVersions := Nil // https://www.scala-sbt.org/1.x/docs/Cross-Build.html#Cross+building+a+project+statefully, ) .aggregate( docs, @@ -101,7 +108,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) scalacOptions -= "-opt:l:inline", scalacOptions -= "-opt-inline-from:zio.internal.**", Test / scalacOptions ++= { - if (scalaVersion.value == ScalaDotty) + if (scalaVersion.value == Scala3) Vector("-Yretain-trees", "-Xmax-inlines:128") else Vector.empty @@ -109,15 +116,15 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) libraryDependencies ++= Seq( "dev.zio" %%% "zio" % zioVersion, "dev.zio" %%% "zio-streams" % zioVersion, - "org.scala-lang.modules" %%% "scala-collection-compat" % "2.13.0" % "test", - "dev.zio" %%% "zio-test" % zioVersion % "test", - "dev.zio" %%% "zio-test-sbt" % zioVersion % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.38.3" % "test", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.38.3" % "test", - "io.circe" %%% "circe-core" % circeVersion % "test", - "io.circe" %%% "circe-generic" % circeVersion % "test", - "io.circe" %%% "circe-parser" % circeVersion % "test", - "org.typelevel" %%% "jawn-ast" % "1.6.0" % "test" + "org.scala-lang.modules" %%% "scala-collection-compat" % "2.14.0" % Test, + "dev.zio" %%% "zio-test" % zioVersion % Test, + "dev.zio" %%% "zio-test-sbt" % zioVersion % Test, + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.38.3" % Test, + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.38.3" % Test, + "io.circe" %%% "circe-core" % circeVersion % Test, + "io.circe" %%% "circe-generic" % circeVersion % Test, + "io.circe" %%% "circe-parser" % circeVersion % Test, + "org.typelevel" %%% "jawn-ast" % "1.6.0" % Test ), // scala version specific dependencies libraryDependencies ++= { @@ -241,7 +248,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) libraryDependencies ++= Seq( ("org.scala-js" %%% "scalajs-weakreferences" % "1.0.0").cross(CrossVersion.for3Use2_13), "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, - "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % "test" + "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % Test ) ) .nativeSettings(nativeSettings) @@ -254,7 +261,7 @@ lazy val zioJson = crossProject(JSPlatform, JVMPlatform, NativePlatform) ), libraryDependencies ++= Seq( "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, - "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % "test" + "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTimeVersion % Test ) ) .enablePlugins(BuildInfoPlugin) @@ -291,10 +298,10 @@ lazy val zioJsonYaml = project .settings( libraryDependencies ++= Seq( "org.yaml" % "snakeyaml" % "2.5", - "org.scala-lang.modules" %% "scala-collection-compat" % "2.13.0", + "org.scala-lang.modules" %% "scala-collection-compat" % "2.14.0", "dev.zio" %% "zio" % zioVersion, - "dev.zio" %% "zio-test" % zioVersion % "test", - "dev.zio" %% "zio-test-sbt" % zioVersion % "test" + "dev.zio" %% "zio-test" % zioVersion % Test, + "dev.zio" %% "zio-test-sbt" % zioVersion % Test ), testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) @@ -308,12 +315,12 @@ lazy val zioJsonMacros = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings(crossProjectSettings) .settings(macroExpansionSettings) .settings( - crossScalaVersions -= ScalaDotty, + crossScalaVersions -= Scala3, scalacOptions -= "-Xfatal-warnings", // not quite ready. libraryDependencies ++= Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided, - "dev.zio" %%% "zio-test" % zioVersion % "test", - "dev.zio" %%% "zio-test-sbt" % zioVersion % "test" + "dev.zio" %%% "zio-test" % zioVersion % Test, + "dev.zio" %%% "zio-test-sbt" % zioVersion % Test ), testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) @@ -334,9 +341,9 @@ lazy val zioJsonInteropHttp4s = project "org.http4s" %% "http4s-dsl" % "0.23.33", "dev.zio" %% "zio" % zioVersion, "org.typelevel" %% "cats-effect" % "3.6.3", - "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % "test", - "dev.zio" %% "zio-test" % zioVersion % "test", - "dev.zio" %% "zio-test-sbt" % zioVersion % "test" + "dev.zio" %% "zio-interop-cats" % "23.1.0.5" % Test, + "dev.zio" %% "zio-test" % zioVersion % Test, + "dev.zio" %% "zio-test-sbt" % zioVersion % Test ), testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) @@ -351,8 +358,8 @@ lazy val zioJsonInteropRefined = crossProject(JSPlatform, JVMPlatform, NativePla .settings( libraryDependencies ++= Seq( "eu.timepit" %%% "refined" % "0.11.3", - "dev.zio" %%% "zio-test" % zioVersion % "test", - "dev.zio" %%% "zio-test-sbt" % zioVersion % "test" + "dev.zio" %%% "zio-test" % zioVersion % Test, + "dev.zio" %%% "zio-test-sbt" % zioVersion % Test ), testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) @@ -364,11 +371,11 @@ lazy val zioJsonInteropScalaz7x = crossProject(JSPlatform, JVMPlatform, NativePl .settings(stdSettings("zio-json-interop-scalaz7x")) .settings(buildInfoSettings("zio.json.interop.scalaz7x")) .settings( - crossScalaVersions -= ScalaDotty, + crossScalaVersions -= Scala3, libraryDependencies ++= Seq( "org.scalaz" %%% "scalaz-core" % "7.3.8", - "dev.zio" %%% "zio-test" % zioVersion % "test", - "dev.zio" %%% "zio-test-sbt" % zioVersion % "test" + "dev.zio" %%% "zio-test" % zioVersion % Test, + "dev.zio" %%% "zio-test-sbt" % zioVersion % Test ), testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) @@ -386,7 +393,7 @@ lazy val docs = project zioJsonInteropScalaz7x.jvm ) .settings( - crossScalaVersions -= ScalaDotty, + crossScalaVersions -= Scala3, moduleName := "zio-json-docs", scalacOptions += "-Ymacro-annotations", projectName := "ZIO JSON", diff --git a/examples/zio-json-golden/build.sbt b/examples/zio-json-golden/build.sbt index d60a52807..7a057ec43 100644 --- a/examples/zio-json-golden/build.sbt +++ b/examples/zio-json-golden/build.sbt @@ -1,2 +1,2 @@ -scalaVersion := "2.13.16" +scalaVersion := "2.13.17" libraryDependencies += "dev.zio" %% "zio-json-golden" % "0.7.8" diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index de8ec77f7..4e5d40d3b 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -10,27 +10,14 @@ import sbtbuildinfo.* import sbtbuildinfo.BuildInfoKeys.* import sbtcrossproject.CrossPlugin.autoImport.* import sbtdynver.DynVerPlugin.autoImport.previousStableVersion +import scalafix.sbt.ScalafixPlugin.autoImport.scalafixSemanticdb import scala.scalanative.sbtplugin.ScalaNativePlugin.autoImport.* object BuildHelper { - private val versions: Map[String, String] = { - import org.snakeyaml.engine.v2.api.{ Load, LoadSettings } - - import java.util.{ List as JList, Map as JMap } - import scala.jdk.CollectionConverters.* - - val doc = new Load(LoadSettings.builder().build()) - .loadFromReader(scala.io.Source.fromFile(".github/workflows/ci.yml").bufferedReader()) - val yaml = doc.asInstanceOf[JMap[String, JMap[String, JMap[String, JMap[String, JMap[String, JList[String]]]]]]] - val list = yaml.get("jobs").get("test").get("strategy").get("matrix").get("scala").asScala - list.map(v => (v.split('.').take(2).mkString("."), v)).toMap - } - val Scala212: String = versions("2.12") - val Scala213: String = versions("2.13") - val ScalaDotty: String = "3.3.7" - - val SilencerVersion = "1.7.19" + val Scala212: String = "2.12.20" + val Scala213: String = "2.13.17" + val Scala3: String = "3.3.7" private val stdOptions = Seq( "-deprecation", @@ -72,22 +59,22 @@ object BuildHelper { ) val dottySettings = Seq( - crossScalaVersions += ScalaDotty, + crossScalaVersions += Scala3, scalacOptions ++= { - if (scalaVersion.value == ScalaDotty) + if (scalaVersion.value == Scala3) Seq("-noindent") else Seq() }, scalacOptions --= { - if (scalaVersion.value == ScalaDotty) + if (scalaVersion.value == Scala3) Seq("-Xfatal-warnings") else Seq() }, Compile / doc / sources := { val old = (Compile / doc / sources).value - if (scalaVersion.value == ScalaDotty) { + if (scalaVersion.value == Scala3) { Nil } else { old @@ -95,7 +82,7 @@ object BuildHelper { }, Test / parallelExecution := { val old = (Test / parallelExecution).value - if (scalaVersion.value == ScalaDotty) { + if (scalaVersion.value == Scala3) { false } else { old @@ -103,10 +90,6 @@ object BuildHelper { } ) - val scalaReflectSettings = Seq( - libraryDependencies ++= Seq("dev.zio" %%% "izumi-reflect" % "1.0.0-M10") - ) - // Keep this consistent with the version in .core-tests/shared/src/test/scala/REPLSpec.scala val replSettings = makeReplSettings { """|import zio._ @@ -148,7 +131,7 @@ object BuildHelper { def extraOptions(scalaVersion: String, optimize: Boolean) = CrossVersion.partialVersion(scalaVersion) match { - case Some((3, 1)) => + case Some((3, _)) => Seq( "-language:implicitConversions", "-Xignore-scala2-macros" @@ -220,31 +203,23 @@ object BuildHelper { ) def stdSettings(prjName: String) = Seq( - name := s"$prjName", - crossScalaVersions := Seq(Scala212, Scala213, ScalaDotty), - ThisBuild / scalaVersion := Scala213, - ThisBuild / publishTo := { - val centralSnapshots = "https://central.sonatype.com/repository/maven-snapshots/" - if (isSnapshot.value) Some("central-snapshots" at centralSnapshots) - else localStaging.value - }, + name := s"$prjName", + crossScalaVersions := Seq(Scala212, Scala213, Scala3), scalacOptions ++= stdOptions ++ extraOptions(scalaVersion.value, optimize = !isSnapshot.value), libraryDependencies ++= { - if (scalaVersion.value == ScalaDotty) - Seq( - "com.github.ghik" % s"silencer-lib_$Scala213" % SilencerVersion % Provided - ) + if (scalaVersion.value == Scala3) Seq.empty else Seq( - "com.github.ghik" % "silencer-lib" % SilencerVersion % Provided cross CrossVersion.full, - compilerPlugin("com.github.ghik" % "silencer-plugin" % SilencerVersion cross CrossVersion.full), - compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.4" cross CrossVersion.full) + compilerPlugin("org.typelevel" %% "kind-projector" % "0.13.4" cross CrossVersion.full) ) }, versionScheme := Some("early-semver"), - semanticdbEnabled := scalaVersion.value != ScalaDotty, // enable SemanticDB - semanticdbOptions += "-P:semanticdb:synthetics:on", - semanticdbVersion := "4.12.7", + semanticdbEnabled := scalaVersion.value == Scala213, // enable SemanticDB + semanticdbOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, _)) => Seq("-P:semanticdb:synthetics:on") + case _ => Seq.empty + }), + semanticdbVersion := scalafixSemanticdb.revision, // use Scalafix compatible version, Test / parallelExecution := true, incOptions ~= (_.withLogRecompileOnMacro(false)), autoAPIMappings := true, @@ -277,7 +252,7 @@ object BuildHelper { def macroDefinitionSettings = Seq( scalacOptions += "-language:experimental.macros", libraryDependencies ++= { - if (scalaVersion.value == ScalaDotty) Seq() + if (scalaVersion.value == Scala3) Seq() else Seq( "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided", @@ -303,7 +278,7 @@ object BuildHelper { val scalaReflectTestSettings: List[Setting[_]] = List( libraryDependencies ++= { - if (scalaVersion.value == ScalaDotty) + if (scalaVersion.value == Scala3) Seq("org.scala-lang" % "scala-reflect" % Scala213 % Test) else Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value % Test) @@ -311,12 +286,10 @@ object BuildHelper { ) def welcomeMessage = onLoadMessage := { - import scala.Console - - def header(text: String): String = s"${Console.RED}$text${Console.RESET}" + def header(text: String): String = s"${scala.Console.RED}$text${scala.Console.RESET}" - def item(text: String): String = s"${Console.GREEN}> ${Console.CYAN}$text${Console.RESET}" - def subItem(text: String): String = s" ${Console.YELLOW}> ${Console.CYAN}$text${Console.RESET}" + def item(text: String): String = s"${scala.Console.GREEN}> ${scala.Console.CYAN}$text${scala.Console.RESET}" + def subItem(text: String): String = s" ${scala.Console.YELLOW}> ${scala.Console.CYAN}$text${scala.Console.RESET}" s"""|${header(" ________ ___")} |${header("|__ /_ _/ _ \\")} diff --git a/project/build.properties b/project/build.properties index 01a16ed14..a360ccac1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.11.7 +sbt.version = 1.11.7 diff --git a/project/plugins.sbt b/project/plugins.sbt index 47f7e45b2..d359af9f5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -12,5 +12,6 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") addSbtPlugin("pl.project13.scala" % "sbt-jcstress" % "0.2.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.0") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.35") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.4") libraryDependencies += "org.snakeyaml" % "snakeyaml-engine" % "2.10" diff --git a/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala index 9374f0733..9d22edcf4 100644 --- a/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala +++ b/zio-json/jvm/src/test/scala/zio/json/data/geojson/GeoJSON.scala @@ -2,7 +2,6 @@ package zio.json.data.geojson import zio.json._ import zio.json.ast._ -import com.github.ghik.silencer.silent import io.circe.{ Codec, Decoder, Encoder } import io.circe.generic.semiauto.deriveCodec import io.circe.syntax.EncoderOps @@ -28,7 +27,6 @@ package generated { features: List[GeoJSON] // NOTE: recursive ) extends GeoJSON - @silent("Block result was adapted via implicit conversion") object Geometry { implicit lazy val zioJsonJsonDecoder: JsonDecoder[Geometry] = DeriveJsonDecoder.gen[Geometry] @@ -67,7 +65,7 @@ package generated { ) } } - @silent("Block result was adapted via implicit conversion") + object GeoJSON { implicit lazy val zioJsonJsonDecoder: JsonDecoder[GeoJSON] = DeriveJsonDecoder.gen[GeoJSON] @@ -95,8 +93,6 @@ package generated { package handrolled { - import com.github.ghik.silencer.silent - sealed abstract class Geometry final case class Point(coordinates: (Double, Double)) extends Geometry final case class MultiPoint(coordinates: List[(Double, Double)]) extends Geometry @@ -114,7 +110,6 @@ package handrolled { features: List[GeoJSON] // NOTE: recursive ) extends GeoJSON - @silent("Block result was adapted via implicit conversion") object Geometry { // this is an example of a handrolled decoder that avoids using the // backtracking algorithm that is normally used for sealed traits with a @@ -267,7 +262,7 @@ package handrolled { ) } } - @silent("Block result was adapted via implicit conversion") + object GeoJSON { // This uses a hand rolled decoder that guesses the type based on the field // names to protect against attack vectors that put the hint at the end of diff --git a/zio-json/jvm/src/test/scala/zio/json/data/googlemaps/GoogleMaps.scala b/zio-json/jvm/src/test/scala/zio/json/data/googlemaps/GoogleMaps.scala index 1447b2c74..c5719d2b8 100644 --- a/zio-json/jvm/src/test/scala/zio/json/data/googlemaps/GoogleMaps.scala +++ b/zio-json/jvm/src/test/scala/zio/json/data/googlemaps/GoogleMaps.scala @@ -1,6 +1,5 @@ package zio.json.data.googlemaps -import com.github.ghik.silencer.silent import com.github.plokhotnyuk.jsoniter_scala.macros.named import io.circe.Codec import io.circe.generic.semiauto.deriveCodec @@ -22,28 +21,27 @@ final case class DistanceMatrix( status: String ) -@silent("Block result was adapted via implicit conversion") object Value { implicit val zioJsonJsonDecoder: JsonDecoder[Value] = DeriveJsonDecoder.gen[Value] implicit val zioJsonEncoder: JsonEncoder[Value] = DeriveJsonEncoder.gen[Value] implicit val circeCodec: Codec[Value] = deriveCodec } -@silent("Block result was adapted via implicit conversion") + object Elements { implicit val zioJsonJsonDecoder: JsonDecoder[Elements] = DeriveJsonDecoder.gen[Elements] implicit val zioJsonEncoder: JsonEncoder[Elements] = DeriveJsonEncoder.gen[Elements] implicit val circeCodec: Codec[Elements] = deriveCodec } -@silent("Block result was adapted via implicit conversion") + object Rows { implicit val zioJsonJsonDecoder: JsonDecoder[Rows] = DeriveJsonDecoder.gen[Rows] implicit val zioJsonEncoder: JsonEncoder[Rows] = DeriveJsonEncoder.gen[Rows] implicit val circeCodec: Codec[Rows] = deriveCodec } -@silent("Block result was adapted via implicit conversion") + object DistanceMatrix { implicit val zioJsonJsonDecoder: JsonDecoder[DistanceMatrix] = DeriveJsonDecoder.gen[DistanceMatrix] diff --git a/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala b/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala index de2f75a38..0b289aaef 100644 --- a/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala +++ b/zio-json/jvm/src/test/scala/zio/json/data/twitter/Twitter.scala @@ -1,6 +1,5 @@ package zio.json.data.twitter -import com.github.ghik.silencer.silent import io.circe.Codec import io.circe.generic.semiauto.deriveCodec import zio.json._ @@ -11,7 +10,6 @@ case class Urls( display_url: String, indices: List[Int] ) -@silent("Block result was adapted via implicit conversion") object Urls { implicit val jJsonDecoder: JsonDecoder[Urls] = DeriveJsonDecoder.gen[Urls] implicit val jEncoder: JsonEncoder[Urls] = DeriveJsonEncoder.gen[Urls] @@ -19,7 +17,7 @@ object Urls { implicit val circeCodec: Codec[Urls] = deriveCodec } case class Url(urls: List[Urls]) -@silent("Block result was adapted via implicit conversion") + object Url { implicit val jJsonDecoder: JsonDecoder[Url] = DeriveJsonDecoder.gen[Url] implicit val jEncoder: JsonEncoder[Url] = DeriveJsonEncoder.gen[Url] @@ -28,7 +26,7 @@ object Url { } case class UserEntities(url: Url, description: Url) -@silent("Block result was adapted via implicit conversion") + object UserEntities { implicit val jJsonDecoder: JsonDecoder[UserEntities] = DeriveJsonDecoder.gen[UserEntities] implicit val jEncoder: JsonEncoder[UserEntities] = DeriveJsonEncoder.gen[UserEntities] @@ -43,7 +41,7 @@ case class UserMentions( id_str: String, indices: List[Int] ) -@silent("Block result was adapted via implicit conversion") + object UserMentions { implicit val jJsonDecoder: JsonDecoder[UserMentions] = DeriveJsonDecoder.gen[UserMentions] implicit val jEncoder: JsonEncoder[UserMentions] = DeriveJsonEncoder.gen[UserMentions] @@ -95,7 +93,7 @@ case class User( notifications: Boolean, translator_type: String ) -@silent("Block result was adapted via implicit conversion") + object User { implicit val jJsonDecoder: JsonDecoder[User] = DeriveJsonDecoder.gen[User] implicit val jEncoder: JsonEncoder[User] = DeriveJsonEncoder.gen[User] @@ -109,7 +107,7 @@ case class Entities( user_mentions: List[UserMentions], urls: List[Urls] ) -@silent("Block result was adapted via implicit conversion") + object Entities { implicit val jJsonDecoder: JsonDecoder[Entities] = DeriveJsonDecoder.gen[Entities] implicit val jEncoder: JsonEncoder[Entities] = DeriveJsonEncoder.gen[Entities] @@ -143,7 +141,7 @@ case class RetweetedStatus( possibly_sensitive: Boolean, lang: String ) -@silent("Block result was adapted via implicit conversion") + object RetweetedStatus { implicit val jJsonDecoder: JsonDecoder[RetweetedStatus] = DeriveJsonDecoder.gen[RetweetedStatus] @@ -181,7 +179,6 @@ case class Tweet( lang: String ) -@silent("Block result was adapted via implicit conversion") object Tweet { implicit val zioJsonJsonDecoder: JsonDecoder[Tweet] = DeriveJsonDecoder.gen[Tweet] implicit val zioJsonEncoder: JsonEncoder[Tweet] = DeriveJsonEncoder.gen[Tweet]