diff --git a/README.md b/README.md index 8f41c2386..6a334a90d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Actions Build](https://github.com/plokhotnyuk/jsoniter-scala/workflows/build/badge.svg)](https://github.com/plokhotnyuk/jsoniter-scala/actions) [![Scala Steward](https://img.shields.io/badge/Scala_Steward-helping-brightgreen.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org) [![Gitter Chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/plokhotnyuk/jsoniter-scala?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Maven Central](https://img.shields.io/badge/maven--central-2.34.1-blue.svg)](https://repo1.maven.org/maven2/com/github/plokhotnyuk/jsoniter-scala/) +[![Maven Central](https://img.shields.io/badge/maven--central-2.35.0-blue.svg)](https://repo1.maven.org/maven2/com/github/plokhotnyuk/jsoniter-scala/) Scala macros for compile-time generation of safe and ultra-fast JSON codecs. @@ -159,6 +159,7 @@ view of an error context - No extra buffering is required when parsing from `java.io.InputStream`/`java.io.FileInputStream` or serializing to `java.io.OutputStream`/`java.io.FileOuputStream` - Using black box macros only for codec generation ensures that your types will never be changed +- Generated code is ordered to preserve checksums and maximize hit rate for remote caches of build tools - Ability to print generated code for codecs using an implicit val of `CodecMakerConfig.PrintCodec` type in a scope of codec derivation - No dependencies on extra libraries in _runtime_ excluding Scala's `scala-library` (all platforms) and @@ -248,16 +249,16 @@ list of sbt dependencies: ```sbt libraryDependencies ++= Seq( // Use the %%% operator instead of %% for Scala.js and Scala Native - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.34.1", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.35.0", // Use the "provided" scope instead when the "compile-internal" scope is not supported - "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.34.1" % "compile-internal" + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.35.0" % "compile-internal" ) ``` In the beginning of Scala CLI script use "dep" scope for the core library or "compileOnly.dep" scope for the macros libary: ```scala -//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.34.1" -//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.34.1" +//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.35.0" +//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.35.0" ``` Derive a codec for the top-level type that need to be parsed or serialized: diff --git a/jsoniter-scala-core/js/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala b/jsoniter-scala-core/js/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala index d5b5bb524..09407a014 100644 --- a/jsoniter-scala-core/js/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala +++ b/jsoniter-scala-core/js/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala @@ -120,6 +120,8 @@ final class JsonReader private[jsoniter_scala]( /** * Sets the current read head position as a mark. + * Should be followed by `restMark()` or `rollbackToMark()` calls. + * Pair of `setMark()` and `restMark()` or `setMark()` and `rollbackToMark()` calls cannot be nested. */ def setMark(): Unit = mark = head @@ -141,7 +143,7 @@ final class JsonReader private[jsoniter_scala]( /** * Rolls back the read head position to the previously set mark. * - * @throws java.lang.IllegalStateException in cases of reaching the end of input or invalid encoding of JSON key + * @throws java.lang.IllegalStateException in case of calling without preceding call of 'setMark()' */ def rollbackToMark(): Unit = { if (mark < 0) missingSetMarkOperation() @@ -149,6 +151,16 @@ final class JsonReader private[jsoniter_scala]( mark = -1 } + /** + * Reset mark without changing of the read head position. + * + * @throws java.lang.IllegalStateException in case of calling without preceding call of 'setMark()' + */ + def resetMark(): Unit = { + if (mark < 0) missingSetMarkOperation() + mark = -1 + } + /** * Reads a JSON key into the internal char buffer and returns the length of the key. * diff --git a/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala b/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala index 96f040e2c..fe17b46a1 100644 --- a/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala +++ b/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala @@ -119,8 +119,10 @@ final class JsonReader private[jsoniter_scala]( } /** - * Sets the current read head position as a mark. - */ + * Sets the current read head position as a mark. + * Should be followed by `restMark()` or `rollbackToMark()` calls. + * Pair of `setMark()` and `restMark()` or `setMark()` and `rollbackToMark()` calls cannot be nested. + */ def setMark(): Unit = mark = head /** @@ -139,16 +141,26 @@ final class JsonReader private[jsoniter_scala]( } /** - * Rolls back the read head position to the previously set mark. - * - * @throws java.lang.IllegalStateException in cases of reaching the end of input or invalid encoding of JSON key - */ + * Rolls back the read head position to the previously set mark. + * + * @throws java.lang.IllegalStateException in case of calling without preceding call of 'setMark()' + */ def rollbackToMark(): Unit = { if (mark < 0) missingSetMarkOperation() head = mark mark = -1 } + /** + * Reset mark without changing of the read head position. + * + * @throws java.lang.IllegalStateException in case of calling without preceding call of 'setMark()' + */ + def resetMark(): Unit = { + if (mark < 0) missingSetMarkOperation() + mark = -1 + } + /** * Reads a JSON key into the internal char buffer and returns the length of the key. * diff --git a/jsoniter-scala-core/native/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala b/jsoniter-scala-core/native/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala index 96f040e2c..fe17b46a1 100644 --- a/jsoniter-scala-core/native/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala +++ b/jsoniter-scala-core/native/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReader.scala @@ -119,8 +119,10 @@ final class JsonReader private[jsoniter_scala]( } /** - * Sets the current read head position as a mark. - */ + * Sets the current read head position as a mark. + * Should be followed by `restMark()` or `rollbackToMark()` calls. + * Pair of `setMark()` and `restMark()` or `setMark()` and `rollbackToMark()` calls cannot be nested. + */ def setMark(): Unit = mark = head /** @@ -139,16 +141,26 @@ final class JsonReader private[jsoniter_scala]( } /** - * Rolls back the read head position to the previously set mark. - * - * @throws java.lang.IllegalStateException in cases of reaching the end of input or invalid encoding of JSON key - */ + * Rolls back the read head position to the previously set mark. + * + * @throws java.lang.IllegalStateException in case of calling without preceding call of 'setMark()' + */ def rollbackToMark(): Unit = { if (mark < 0) missingSetMarkOperation() head = mark mark = -1 } + /** + * Reset mark without changing of the read head position. + * + * @throws java.lang.IllegalStateException in case of calling without preceding call of 'setMark()' + */ + def resetMark(): Unit = { + if (mark < 0) missingSetMarkOperation() + mark = -1 + } + /** * Reads a JSON key into the internal char buffer and returns the length of the key. * diff --git a/jsoniter-scala-core/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReaderSpec.scala b/jsoniter-scala-core/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReaderSpec.scala index d56350cb1..987200039 100644 --- a/jsoniter-scala-core/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReaderSpec.scala +++ b/jsoniter-scala-core/shared/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonReaderSpec.scala @@ -3209,7 +3209,7 @@ class JsonReaderSpec extends AnyWordSpec with Matchers with ScalaCheckPropertyCh """too long part of input exceeded 'maxBufSize', offset: 0x02000001""") } } - "JsonReader.setMark and JsonReader.rollbackToMark" should { + "JsonReader.setMark, JsonReader.rollbackToMark and JsonReader.resetMark" should { "store current position of parsing and return back to it" in { def check[A](n: Int, s2: String)(f: JsonReader => A): Unit = { val jsonReader = reader("{}" + fill(' ', n) + s2) @@ -3217,8 +3217,14 @@ class JsonReaderSpec extends AnyWordSpec with Matchers with ScalaCheckPropertyCh jsonReader.skip() jsonReader.setMark() f(jsonReader) - jsonReader.rollbackToMark() - jsonReader.nextToken().toChar shouldBe s2.charAt(0) + if ((n & 1) == 0) { + jsonReader.rollbackToMark() + jsonReader.hasRemaining() shouldBe true + jsonReader.nextToken().toChar shouldBe s2.charAt(0) + } else { + jsonReader.resetMark() + jsonReader.hasRemaining() shouldBe false + } } forAll(Gen.size, minSuccessful(1000)) { n => @@ -3236,6 +3242,12 @@ class JsonReaderSpec extends AnyWordSpec with Matchers with ScalaCheckPropertyCh assert(intercept[IllegalStateException](jsonReader.rollbackToMark()) .getMessage.startsWith("expected preceding call of 'setMark()'")) } + "throw exception in case of resetMark was called before setMark" in { + val jsonReader = reader("{}") + jsonReader.skip() + assert(intercept[IllegalStateException](jsonReader.resetMark()) + .getMessage.startsWith("expected preceding call of 'setMark()'")) + } } "JsonReader.skipToKey" should { "return true in case of key is found and set current position of parsing to its value" in { diff --git a/jsoniter-scala-examples/example01.sc b/jsoniter-scala-examples/example01.sc index fbefd9688..1df0f5e98 100644 --- a/jsoniter-scala-examples/example01.sc +++ b/jsoniter-scala-examples/example01.sc @@ -1,5 +1,5 @@ -//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.34.1" -//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.34.1" +//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.35.0" +//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.35.0" import com.github.plokhotnyuk.jsoniter_scala.macros._ import com.github.plokhotnyuk.jsoniter_scala.core._ diff --git a/jsoniter-scala-examples/example02.sc b/jsoniter-scala-examples/example02.sc index b44bac8d0..09bc6824f 100644 --- a/jsoniter-scala-examples/example02.sc +++ b/jsoniter-scala-examples/example02.sc @@ -1,4 +1,4 @@ -//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.34.1" +//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.35.0" import com.github.plokhotnyuk.jsoniter_scala.core._ diff --git a/jsoniter-scala-examples/example03.sc b/jsoniter-scala-examples/example03.sc index dbee9ee6c..4548232d2 100644 --- a/jsoniter-scala-examples/example03.sc +++ b/jsoniter-scala-examples/example03.sc @@ -1,5 +1,5 @@ -//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.34.1" -//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.34.1" +//> using dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core::2.35.0" +//> using compileOnly.dep "com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::2.35.0" import com.github.plokhotnyuk.jsoniter_scala.macros._ import com.github.plokhotnyuk.jsoniter_scala.core._ diff --git a/jsoniter-scala-macros/shared/src/test/scala-3/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMakerNewTypeSpec.scala b/jsoniter-scala-macros/shared/src/test/scala-3/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMakerNewTypeSpec.scala index aa25240ad..4e3de39d1 100644 --- a/jsoniter-scala-macros/shared/src/test/scala-3/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMakerNewTypeSpec.scala +++ b/jsoniter-scala-macros/shared/src/test/scala-3/com/github/plokhotnyuk/jsoniter_scala/macros/JsonCodecMakerNewTypeSpec.scala @@ -153,6 +153,30 @@ class JsonCodecMakerNewTypeSpec extends VerifyingSpec { verifySerDeser(summon[JsonValueCodec[Group]], group, """[{"type":"A","a":"Hi"},{"type":"B","b":"Bye"},{"type":"A","a":3.4},{"type":"B","b":4.5},{"type":"A","a":true},{"type":"B","b":false}]""") } + "serialize and deserialize a Scala3 union type using a custom codec with setMark, resetMark, and rollbackToMark calls" in { + implicit val intOrBigDecimalCodec: JsonValueCodec[Int | BigDecimal] = + new JsonValueCodec[Int | BigDecimal]: + def decodeValue(in: JsonReader, default: Int | BigDecimal): Int | BigDecimal = + in.setMark() + try { + val a = in.readInt() + in.resetMark() + a + } catch { // use this approach wisely taking in account cost of failed parsing with exception throwing and catching + case _: JsonReaderException => + in.rollbackToMark() + in.readBigDecimal(null) + } + + def encodeValue(x: Int | BigDecimal, out: JsonWriter): Unit = + if (x.isInstanceOf[BigDecimal]) out.writeVal(x.asInstanceOf[BigDecimal]) + else out.writeVal(x.asInstanceOf[Int]) + + def nullValue: Int | BigDecimal = null.asInstanceOf[Int | BigDecimal] + + verifySerDeser(summon[JsonValueCodec[Int | BigDecimal]], 1, "1") + verifySerDeser(summon[JsonValueCodec[Int | BigDecimal]], BigDecimal("1" * 33), "1" * 33) + } "serialize and deserialize recursive Scala3 union types using a custom value codec" in { type JsonPrimitive = String | Int | Double | Boolean | None.type diff --git a/version.sbt b/version.sbt index a5380c362..9f83a5b7a 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -ThisBuild / version := "2.34.1" +ThisBuild / version := "2.35.0"