From 84900833cbfbdd7dd4dbb77bd45c8bcb972d9aeb Mon Sep 17 00:00:00 2001 From: Greg Zoller Date: Wed, 29 May 2024 23:31:31 -0500 Subject: [PATCH] wrapping up --- .github/workflows/ci.yml | 96 +++--- .github/workflows/coveralls.yml | 59 ++++ README.md | 29 +- benchmark/build.sbt | 2 +- .../src/main/scala/co.blocke/Benchmark.scala | 25 +- build.sbt | 17 +- project/plugins.sbt | 3 +- .../scala/co.blocke.scalajack/ScalaJack.scala | 2 - .../internal/CodePrinter.scala | 2 + .../{json/reading => internal}/Numbers.scala | 8 +- .../json/JsonCodecMaker.scala | 111 +++--- .../co.blocke.scalajack/json/package.scala | 2 - .../json/reading/JsonSource.scala | 3 +- .../json/schema/JsonSchema.scala | 323 ------------------ .../json/schema/Schema.scala | 114 ------- .../json/writing/JsonOutput.scala | 5 - .../json/collections/MapSpec.scala | 124 ++++++- .../json/collections/Model.scala | 9 +- .../json/collections/SeqSetArraySpec.scala | 37 ++ .../json/collections/TupleSpec.scala | 7 + .../co.blocke.scalajack/json/misc/Model.scala | 1 + .../json/misc/OptionSpec.scala | 7 + .../json/misc/TrySpec.scala | 7 + .../json/primitives/ScalaPrimSpec.scala | 50 +++ .../json/primitives/SimpleSpec.scala | 4 +- 25 files changed, 443 insertions(+), 604 deletions(-) create mode 100644 .github/workflows/coveralls.yml rename src/main/scala/co.blocke.scalajack/{json/reading => internal}/Numbers.scala (99%) delete mode 100644 src/main/scala/co.blocke.scalajack/json/schema/JsonSchema.scala delete mode 100644 src/main/scala/co.blocke.scalajack/json/schema/Schema.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f73ca6f3..3ce76a45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,66 +17,57 @@ on: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +concurrency: + group: ${{ github.workflow }} @ ${{ github.ref }} + cancel-in-progress: true + jobs: build: name: Build and Test strategy: matrix: - os: [ubuntu-latest, windows-latest] - scala: [3.3.0] - java: [zulu@8] + os: [ubuntu-latest] + scala: [3.4.2] + java: [zulu@21] runs-on: ${{ matrix.os }} + timeout-minutes: 60 steps: - - name: Ignore line ending differences in git - if: contains(runner.os, 'windows') - shell: bash - run: git config --global core.autocrlf false - - name: Checkout current branch (full) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup Java (zulu@8) - if: matrix.java == 'zulu@8' - uses: actions/setup-java@v3 + - name: Setup Java (zulu@21) + id: setup-java-zulu-21 + if: matrix.java == 'zulu@21' + uses: actions/setup-java@v4 with: distribution: zulu - java-version: 8 + java-version: 21 + cache: sbt - - name: Cache sbt - uses: actions/cache@v3 - with: - path: | - ~/.sbt - ~/.ivy2/cache - ~/.coursier/cache/v1 - ~/.cache/coursier/v1 - ~/AppData/Local/Coursier/Cache/v1 - ~/Library/Caches/Coursier/v1 - key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} + - name: sbt update + if: matrix.java == 'zulu@21' && steps.setup-java-zulu-21.outputs.cache-hit == 'false' + run: sbt +update - name: Check that workflows are up to date - shell: bash run: sbt githubWorkflowCheck - name: Build project - shell: bash - run: sbt '++${{ matrix.scala }}' test + run: sbt '++ ${{ matrix.scala }}' test - name: Make target directories if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) - shell: bash run: mkdir -p target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) - shell: bash run: tar cf targets.tar target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }} path: targets.tar @@ -88,44 +79,33 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [3.3.0] - java: [zulu@8] + java: [zulu@21] runs-on: ${{ matrix.os }} steps: - - name: Ignore line ending differences in git - if: contains(runner.os, 'windows') - run: git config --global core.autocrlf false - - name: Checkout current branch (full) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup Java (zulu@8) - if: matrix.java == 'zulu@8' - uses: actions/setup-java@v3 + - name: Setup Java (zulu@21) + id: setup-java-zulu-21 + if: matrix.java == 'zulu@21' + uses: actions/setup-java@v4 with: distribution: zulu - java-version: 8 + java-version: 21 + cache: sbt - - name: Cache sbt - uses: actions/cache@v3 - with: - path: | - ~/.sbt - ~/.ivy2/cache - ~/.coursier/cache/v1 - ~/.cache/coursier/v1 - ~/AppData/Local/Coursier/Cache/v1 - ~/Library/Caches/Coursier/v1 - key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - - - name: Download target directories (3.3.0) - uses: actions/download-artifact@v3 + - name: sbt update + if: matrix.java == 'zulu@21' && steps.setup-java-zulu-21.outputs.cache-hit == 'false' + run: sbt +update + + - name: Download target directories (3.4.2) + uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.0 + name: target-${{ matrix.os }}-${{ matrix.java }}-3.4.2 - - name: Inflate target directories (3.3.0) + - name: Inflate target directories (3.4.2) run: | tar xf targets.tar rm targets.tar @@ -136,4 +116,4 @@ jobs: SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} PGP_SECRET: ${{ secrets.PGP_SECRET }} - run: sbt '++${{ matrix.scala }}' ci-release + run: sbt ci-release diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml new file mode 100644 index 00000000..b6d50810 --- /dev/null +++ b/.github/workflows/coveralls.yml @@ -0,0 +1,59 @@ +name: Coveralls Publish + +on: + push: + branches: [main] + tags: ["v*"] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + +concurrency: + group: ${{ github.workflow }} @ ${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and Test + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + scala: [3.4.2] + java: [zulu@21] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - name: Ignore line ending differences in git + if: contains(runner.os, 'windows') + shell: bash + run: git config --global core.autocrlf false + + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (zulu@21) + id: setup-java-zulu-21 + if: matrix.java == 'zulu@21' + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 21 + cache: sbt + + - name: sbt update + if: matrix.java == 'zulu@21' && steps.setup-java-zulu-21.outputs.cache-hit == 'false' + shell: bash + run: sbt +update + + - name: Build project + run: sbt '++ ${{ matrix.scala }}' coverage test + + - run: sbt '++ ${{ matrix.scala }}' coverageReport + + - name: Coveralls + uses: coverallsapp/github-action@v2 + with: + git-branch: main \ No newline at end of file diff --git a/README.md b/README.md index 5d53c2ea..808b39da 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,11 @@ [![Maven Central](https://maven-badges.herokuapp.com/maven-central/co.blocke/scalajack_3/badge.svg)](https://search.maven.org/artifact/co.blocke/scalajack_3/8.0.0/jar) ScalaJack 8 is an all-new ScalaJack serializer implemenation built on Scala 3. For Scala 2.13 ScalaJack, please use the frozen version 6.2.0. ScalaJack 8 is built -using Scala 3.4.1 on JDK 21 LTS version. +using Scala 3.4.2 on JDK 21 LTS version. This is done to be as current as possible and also because Scala 3.4.2 provides improvements to code test coverage instrumentation. -ScalaJack is a very fast, seamless serialization engine for unstructured data types designed to require a bare minimum of extra code -to serialize a class. ScalaJack supports JSON in its focus on over-the-wire data and message/event transport. (We looked at offering MsgPack support, but to our surprise benchmarks -showed that MsgPack serialization had about 25% slower write performance and 45% slower read performance than JSON, so for now we're sticking with just JSON support.) - -Advanced Features: - -- Handles tuples -- 'Any' support -- Handles default values for case class fields -- Rich configuration of trait type hint/value -- Supports value classes -- Sealed trait-style enumerations +ScalaJack is a very fast, seamless serialization engine for non-schema data designed to require a bare minimum of extra code +to serialize a class. ScalaJack currently only supports JSON, however when we looked at adding MsgPack support to our great surprise benchmarks +showed that MsgPack serialization had about 25% slower write performance and 45% slower read performance than JSON, so we're sticking with JSON for the time being. ## Use ScalaJack is extremely simple to use. @@ -27,7 +18,7 @@ Include the following in your build.sbt: ``` libraryDependencies ++= Seq("co.blocke" %% "scalajack" % SJ_VERSION) ``` -Now you're good to go! Let's use ScalaJack in your project to serialize/de-serialize a case class object into JSON: +Now you're good to go! Let's use ScalaJack in your project to serialize/deserialize a case class object into JSON: ```scala // File1.scala case class Person(name: String, age: Int) @@ -44,15 +35,15 @@ sjPerson.fromJson(js) // re-constitutes original Person Couldn't be simpler! | **NOTE:** Classes must be defined in a different file from where ScalaJack is called. -| This is a Scala macro requirement, not a ScalaJack "ism" +| This is a Scala macro requirement, not a ScalaJack limitation. ### A word about performance... -Compared to pre-8.0 ScalaJack, which used Scala 2.x runtime reflection, ScalaJack is dramatically faster in almost every case. How's this work? ScalaJack 8 uses macros, that at compile-time generate all the serialization code for you (the codecs). It's very much like writing hand-tooled, field-by-field serialization code yourself, except ScalaJack does it at compile-time. Wherever you see ```sjCodecOf``` is where the compiler will generate all the serialization code. **(That also means try not to use sjCodecOf more than once for any given class or you'll generate a lot of redundant code!)** +Compared to pre-8.0 ScalaJack, which used Scala 2.x runtime reflection, ScalaJack is dramatically faster in almost every case. How does this work? ScalaJack 8 uses compile-time macros to generate all the serialization code for you (the codecs). It's very much like writing hand-tooled, field-by-field serialization code yourself, except ScalaJack does it at compile-time. Wherever you see ```sjCodecOf``` is where the compiler will generate all the serialization code. **(That also means you should try not to use sjCodecOf more than once for any given class or you'll generate a lot of redundant code!)** ### Easy codecs You only need to worry about generating codecs for your top-most level classes. Some serialization libraries require all nested classes in an object hierarchy to be -specifically called out for code generation, which can get pretty burdensome. ScalaJack doesn't require this. For example: +specifically called out for codec generation, which can get pretty burdensome. ScalaJack doesn't require this. For example: ```scala case class Dog(name: String, numLegs: Int) @@ -78,7 +69,7 @@ val js = sjFoo.fromJson(someJson) In a non-macro program (e.g. something using Scala 2 runtime reflection) let's say you add a new field to class Foo in File1.scala. You naturally expect sbt to re-compile this file, and anything that depends on Foo, and the changes will be picked up in your program, and all will be well. -That's **not** necessarily what happens with macros! Remember, the macro code is run/expnded at compile-time. File2.scala needs to be re-compiled because the macro that gets expanded at sjCodecOf[Foo] needs to be re-generated to pick up your changes to Foo class in File1.scala. **Unfortunately sbt cna't detect up this dependency!** If you don't know any better you'll just re-run your program after a change to File1.scala, like normal, and you'll get a spectacular exception with exotic errors that won't mean much to you. The simple, but non-intuitive, solution is you need to also recompile File2.scala. +That's **not** necessarily what happens with macros! Remember, the macro code is run/expnded at compile-time. File2.scala needs to be re-compiled because the macro that gets expanded at sjCodecOf[Foo] needs to be re-generated to pick up your changes to Foo class in File1.scala. **Unfortunately sbt can't detect this dependency!** If you don't know any better you'll just re-run your program after a change to File1.scala, like normal, and you'll get a spectacular exception with exotic errors that won't mean much to you. The simple, but non-intuitive, solution is you need to also recompile File2.scala. This means you will be doing more re-compiling with macro-based code than you would without the macros. It's an unfortunate cost of inconvenience, but the payoff is a *dramatic* gain in speed at runtime, and in the case of reflection in Scala 3, using macros is the only way to accomplish reflection, so there really isn't an alternative. @@ -97,7 +88,7 @@ This means you will be doing more re-compiling with macro-based code than you wo ### Notes: -* 8.0.0 -- Rebuild on Scala 3.4.1 and major refactor of ScalaJack 7.0 +* 8.0.0 -- Rebuild on Scala 3.4.2 and deep refactor of ScalaJack 7.0 * 7.0.3 -- Rebuild on Scala 3.2.1 * 7.0.1 -- GA release of ScalaJack 7 for Scala 3. * 7.0.0-M2 -- Initial release for Scala 3 \ No newline at end of file diff --git a/benchmark/build.sbt b/benchmark/build.sbt index 42abc17a..548c1c1e 100644 --- a/benchmark/build.sbt +++ b/benchmark/build.sbt @@ -13,7 +13,7 @@ val compilerOptions = Seq( val circeVersion = "0.15.0-M1" val scalaTestVersion = "3.2.11" -ThisBuild / scalaVersion := "3.4.1" +ThisBuild / scalaVersion := "3.4.2" def priorTo2_13(scalaVersion: String): Boolean = CrossVersion.partialVersion(scalaVersion) match { diff --git a/benchmark/src/main/scala/co.blocke/Benchmark.scala b/benchmark/src/main/scala/co.blocke/Benchmark.scala index 793e3914..72e57d30 100644 --- a/benchmark/src/main/scala/co.blocke/Benchmark.scala +++ b/benchmark/src/main/scala/co.blocke/Benchmark.scala @@ -44,21 +44,20 @@ trait HandTooledWritingBenchmark { @OutputTimeUnit(TimeUnit.SECONDS) class ReadingBenchmark extends ScalaJackZ.ScalaJackReadingBenchmark - // with CirceZ.CirceReadingBenchmark - // extends JsoniterZ.JsoniterReadingBenchmark - // with ZIOZ.ZIOJsonReadingBenchmark - // with PlayZ.PlayReadingBenchmark - // with ArgonautZ.ArgonautReadingBenchmark + with CirceZ.CirceReadingBenchmark + with JsoniterZ.JsoniterReadingBenchmark + with ZIOZ.ZIOJsonReadingBenchmark + with PlayZ.PlayReadingBenchmark + with ArgonautZ.ArgonautReadingBenchmark @State(Scope.Thread) @BenchmarkMode(Array(Mode.Throughput)) @OutputTimeUnit(TimeUnit.SECONDS) class WritingBenchmark - // extends HandTooledWritingBenchmark - // extends CirceZ.CirceWritingBenchmark - // extends ScalaJackZ.MsgPackWritingBenchmark - extends ScalaJackZ.ScalaJackWritingBenchmark - // extends JsoniterZ.JsoniterWritingBenchmark - // with ZIOZ.ZIOJsonWritingBenchmark - // with PlayZ.PlayWritingBenchmark - // with ArgonautZ.ArgonautWritingBenchmark + extends HandTooledWritingBenchmark + with CirceZ.CirceWritingBenchmark + with ScalaJackZ.ScalaJackWritingBenchmark + with JsoniterZ.JsoniterWritingBenchmark + with ZIOZ.ZIOJsonWritingBenchmark + with PlayZ.PlayWritingBenchmark + with ArgonautZ.ArgonautWritingBenchmark diff --git a/build.sbt b/build.sbt index 8a564953..aa197695 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,6 @@ import org.typelevel.sbt.gha.JavaSpec.Distribution.Zulu +import scoverage.ScoverageKeys._ + lazy val isCI = sys.env.get("CI").contains("true") inThisBuild(List( @@ -19,7 +21,8 @@ inThisBuild(List( name := "scalajack" ThisBuild / organization := "co.blocke" -ThisBuild / scalaVersion := "3.4.1" +ThisBuild / scalaVersion := "3.4.2" +ThisBuild / githubWorkflowScalaVersions := Seq("3.4.2") lazy val root = project .in(file(".")) @@ -35,7 +38,7 @@ lazy val root = project Test / parallelExecution := false, scalafmtOnCompile := !isCI, libraryDependencies ++= Seq( - "co.blocke" %% "scala-reflection" % "2.0.6", + "co.blocke" %% "scala-reflection" % "2.0.8", "org.apache.commons" % "commons-text" % "1.11.0", "io.github.kitlangton" %% "neotype" % "0.0.9", "org.scalatest" %% "scalatest" % "3.2.17" % Test, @@ -44,7 +47,7 @@ lazy val root = project ) ) -ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec(Zulu, "8")) +ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec(Zulu, "21")) ThisBuild / githubWorkflowOSes := Seq("ubuntu-latest") ThisBuild / githubWorkflowPublishTargetBranches := Seq( RefPredicate.Equals(Ref.Branch("main")), @@ -77,7 +80,13 @@ lazy val compilerOptions = Seq( "-feature", "-language:implicitConversions", "-deprecation", - // "-explain", + // "-explain",' + "-coverage-exclude-files", + ".*SJConfig.*", + "-coverage-exclude-classlikes", + ".*internal.*", + "-coverage-exclude-classlikes", + ".*AnyWriter", "-encoding", "utf8" ) diff --git a/project/plugins.sbt b/project/plugins.sbt index 2424290e..0b0faec3 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,6 @@ addSbtPlugin("co.blocke" % "gitflow-packager" % "0.1.32") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.11") addSbtPlugin("org.typelevel" % "sbt-typelevel-sonatype-ci-release" % "0.5.0-M6") -addSbtPlugin("org.typelevel" % "sbt-typelevel-github-actions" % "0.4.16") +addSbtPlugin("org.typelevel" % "sbt-typelevel-github-actions" % "0.7.1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.12") \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/ScalaJack.scala b/src/main/scala/co.blocke.scalajack/ScalaJack.scala index 432fe60e..9b36c0be 100644 --- a/src/main/scala/co.blocke.scalajack/ScalaJack.scala +++ b/src/main/scala/co.blocke.scalajack/ScalaJack.scala @@ -20,8 +20,6 @@ case class ScalaJack[T](jsonCodec: JsonCodec[T]): object ScalaJack: - def apply[A](implicit a: ScalaJack[A]): ScalaJack[A] = a - // ----- Use default JsonConfig inline def sjCodecOf[T]: ScalaJack[T] = ${ codecOfImpl[T] } def codecOfImpl[T: Type](using Quotes): Expr[ScalaJack[T]] = diff --git a/src/main/scala/co.blocke.scalajack/internal/CodePrinter.scala b/src/main/scala/co.blocke.scalajack/internal/CodePrinter.scala index 162bee24..fde8eb7f 100644 --- a/src/main/scala/co.blocke.scalajack/internal/CodePrinter.scala +++ b/src/main/scala/co.blocke.scalajack/internal/CodePrinter.scala @@ -10,6 +10,7 @@ import scala.quoted.* * sj[Record] * } */ +//$COVERAGE-OFF$3rd party library object CodePrinter { inline def structure[A](inline value: A) = ${ structureMacro('value) } @@ -30,3 +31,4 @@ object CodePrinter { value } } +//$COVERAGE-ON$ diff --git a/src/main/scala/co.blocke.scalajack/json/reading/Numbers.scala b/src/main/scala/co.blocke.scalajack/internal/Numbers.scala similarity index 99% rename from src/main/scala/co.blocke.scalajack/json/reading/Numbers.scala rename to src/main/scala/co.blocke.scalajack/internal/Numbers.scala index 23bfe631..fba4ba19 100644 --- a/src/main/scala/co.blocke.scalajack/json/reading/Numbers.scala +++ b/src/main/scala/co.blocke.scalajack/internal/Numbers.scala @@ -14,11 +14,14 @@ * limitations under the License. */ package co.blocke.scalajack -package json -package reading +package internal import java.io.* import scala.util.control.NoStackTrace +import json.reading.JsonSource +import json.* + +// $COVERAGE-OFF$3rd party library /** Total, fast, number parsing. * @@ -834,3 +837,4 @@ object UnsafeNumbers { private val longunderflow: Long = Long.MinValue / 10L private val longoverflow: Long = Long.MaxValue / 10L } +// $COVERAGE-ON$ diff --git a/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala b/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala index efb55add..5e2f0fbb 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala @@ -286,6 +286,22 @@ object JsonCodecMaker: } } + case t: IterableRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[e] => + val tin = in.asInstanceOf[Expr[Iterable[e]]] + '{ + if $tin == null then $out.burpNull() + else + $out.startArray() + $tin.foreach { i => + ${ genWriteVal('{ i }, t.elementRef.asInstanceOf[RTypeRef[e]], out) } + } + $out.endArray() + } + } + case t: SeqRef[?] => makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => t.elementRef.refType match @@ -791,12 +807,9 @@ object JsonCodecMaker: case t: ObjectRef[?] => '{ $out.value(${ Expr(t.name) }) } case t: AliasRef[?] => - // Special check for RawJson pseudo-type - if lastPart(t.definedType) == "RawJson" then '{ $out.valueRaw(${ aE.asExprOf[RawJson] }) } - else - t.unwrappedType.refType match - case '[e] => - genWriteVal[e](aE.asInstanceOf[Expr[e]], t.unwrappedType.asInstanceOf[RTypeRef[e]], out, inTuple = inTuple) + t.unwrappedType.refType match + case '[e] => + genWriteVal[e](aE.asInstanceOf[Expr[e]], t.unwrappedType.asInstanceOf[RTypeRef[e]], out, inTuple = inTuple) // These are here becaue Enums and their various flavors can be Map keys // (EnumRef handles: Scala 3 enum, Scala 2 Enumeration, Java Enumeration) @@ -1454,31 +1467,23 @@ object JsonCodecMaker: case t: ZoneIdRef => '{ $in.expectString(java.time.ZoneId.of) }.asExprOf[T] case t: ZoneOffsetRef => '{ $in.expectString(java.time.ZoneOffset.of) }.asExprOf[T] - case t: URLRef => '{ $in.expectString((s: String) => new java.net.URL(s)) }.asExprOf[T] + case t: URLRef => '{ $in.expectString((s: String) => new java.net.URI(s).toURL()) }.asExprOf[T] case t: URIRef => '{ $in.expectString((s: String) => new java.net.URI(s)) }.asExprOf[T] case t: UUIDRef => '{ $in.expectString(java.util.UUID.fromString) }.asExprOf[T] case t: AliasRef[?] => - // Special check for RawJson pseudo-type - if lastPart(t.definedType) == "RawJson" then - '{ - val mark = $in.pos - $in.skipValue() - $in.captureMark(mark) - }.asExprOf[T] - else - t.unwrappedType.refType match - case '[e] => - '{ - ${ - genReadVal[e]( - t.unwrappedType.asInstanceOf[RTypeRef[e]], - in, - inTuple, - isMapKey - ) - }.asInstanceOf[T] - } + t.unwrappedType.refType match + case '[e] => + '{ + ${ + genReadVal[e]( + t.unwrappedType.asInstanceOf[RTypeRef[e]], + in, + inTuple, + isMapKey + ) + }.asInstanceOf[T] + } // -------------------- // Options... @@ -1673,15 +1678,6 @@ object JsonCodecMaker: if parsedArray != null then parsedArray.toVector else null }.asExprOf[T] - case '[Seq[?]] => - t.elementRef.refType match - case '[e] => - val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] - '{ - val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) - if parsedArray != null then parsedArray.toSeq - else null - }.asExprOf[T] case '[IndexedSeq[?]] => t.elementRef.refType match case '[e] => @@ -1691,13 +1687,13 @@ object JsonCodecMaker: if parsedArray != null then parsedArray.toIndexedSeq else null }.asExprOf[T] - case '[Iterable[?]] => + case '[Seq[?]] => t.elementRef.refType match case '[e] => val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] '{ val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) - if parsedArray != null then parsedArray.toIterable + if parsedArray != null then parsedArray.toSeq else null }.asExprOf[T] // Catch all, with (slightly) slower type coersion to proper Seq flavor @@ -1711,6 +1707,19 @@ object JsonCodecMaker: else null }.asExprOf[T] + case t: IterableRef[?] => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + val ct = Expr.summon[ClassTag[e]].get + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then + implicit val ctt = $ct + parsedArray.asInstanceOf[Iterable[e]] + else null + }.asExprOf[T] + case t: ArrayRef[?] => t.elementRef.refType match case '[e] => @@ -2080,24 +2089,16 @@ object JsonCodecMaker: // make all the tuple terms, accounting for , and ] detection val tupleTerms = - if t.tupleRefs.length == 1 then - t.tupleRefs(0).refType match + t.tupleRefs.zipWithIndex.map { case (tpart, i) => + tpart.refType match case '[e] => - List('{ - ${ genReadVal[e](t.tupleRefs(0).asInstanceOf[RTypeRef[e]], in, true) } - $in.expectToken(']') - }.asTerm) - else - t.tupleRefs.zipWithIndex.map { case (tpart, i) => - tpart.refType match - case '[e] => - if i == 0 then genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true).asTerm - else - '{ - $in.expectToken(',') - ${ genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true) } - }.asTerm - } + if i == 0 then genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true).asTerm + else + '{ + $in.expectToken(',') + ${ genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true) } + }.asTerm + } '{ if $in.expectNull() then null else diff --git a/src/main/scala/co.blocke.scalajack/json/package.scala b/src/main/scala/co.blocke.scalajack/json/package.scala index af721926..5a78546b 100644 --- a/src/main/scala/co.blocke.scalajack/json/package.scala +++ b/src/main/scala/co.blocke.scalajack/json/package.scala @@ -5,8 +5,6 @@ import scala.util.Failure import scala.quoted.{Expr, Quotes, Type} import java.util.Optional -opaque type RawJson = String - val BUFFER_EXCEEDED: Char = 7 // Old "BELL" ASCII value, used as a marker when we've run off the end of the known world val END_OF_STRING: Char = 3 diff --git a/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala b/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala index 2adac083..8c436ac1 100644 --- a/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala +++ b/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala @@ -3,7 +3,8 @@ package json package reading import scala.annotation.{switch, tailrec} -import co.blocke.scalajack.json.reading.SafeNumbers.double +import co.blocke.scalajack.internal.SafeNumbers.double +import co.blocke.scalajack.internal.UnsafeNumbers object JsonSource: val ull: Array[Char] = "ull".toCharArray diff --git a/src/main/scala/co.blocke.scalajack/json/schema/JsonSchema.scala b/src/main/scala/co.blocke.scalajack/json/schema/JsonSchema.scala deleted file mode 100644 index 85864547..00000000 --- a/src/main/scala/co.blocke.scalajack/json/schema/JsonSchema.scala +++ /dev/null @@ -1,323 +0,0 @@ -package co.blocke.scalajack -package json -package schema - -import java.net.URL -import co.blocke.scala_reflection.* -import co.blocke.scala_reflection.reflect.ReflectOnType -import co.blocke.scala_reflection.reflect.rtypeRefs.* -import org.apache.commons.text.StringEscapeUtils - -// Macro... -import scala.quoted.* - -object JsonSchema: - - inline def of[T](overrides: Map[String, Schema]): Schema = ${ ofImpl[T]('overrides) } - - inline def of[T]: Schema = ${ ofImpl[T]() } - - def ofImpl[T]()(using t: Type[T])(using quotes: Quotes): Expr[Schema] = - import quotes.reflect.* - val classRef = ReflectOnType[T](quotes)(TypeRepr.of[T], true)(using scala.collection.mutable.Map.empty[TypedName, Boolean]) - genSchema(quotes)(classRef, None, '{ Map.empty[String, Schema] }, None, true) - - def ofImpl[T](overrides: Expr[Map[String, Schema]])(using t: Type[T])(using quotes: Quotes): Expr[Schema] = - import quotes.reflect.* - val classRef = ReflectOnType[T](quotes)(TypeRepr.of[T], true)(using scala.collection.mutable.Map.empty[TypedName, Boolean]) - genSchema(quotes)(classRef, None, overrides, None, true) - - private def genSchema[T](quotes: Quotes)( - rt: RTypeRef[T], - context: Option[ScalaFieldInfoRef] = None, - overrides: Expr[Map[String, Schema]], - defaultValue: Option[quotes.reflect.Term] = None, - isInitialSchema: Boolean = false - ): Expr[Schema] = - import quotes.reflect.* - implicit val q: Quotes = quotes - - def typeArgs(tpe: TypeRepr): List[TypeRepr] = tpe match - case AppliedType(_, typeArgs) => typeArgs.map(_.dealias) - case _ => Nil - - val rtName = Expr(rt.name) - '{ - $overrides - .get($rtName) - .getOrElse(${ - rt match - case t: AliasRef[?] => - t.unwrappedType.refType match - case '[e] => - genSchema[e](quotes)(t.unwrappedType.asInstanceOf[RTypeRef[e]], context, overrides, defaultValue) - case t: ArrayRef[?] => - t.refType match - case '[u] => - t.elementRef.refType match - case '[e] => - '{ - ArraySchema( - ${ genSchema[e](quotes)(t.elementRef.asInstanceOf[RTypeRef[e]], context, overrides, None) }, - ${ Expr(context.flatMap(_.annotations.get("minItems")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("maxItems")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("uniqueItems")).flatMap(_.get("value")).map(_.toBoolean)) }, - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - if defaultValue.isDefined then - val codec = JsonCodecMaker.generateCodecFor[u](t.refType.asInstanceOf[RTypeRef[u]], SJConfig) - '{ - val out = new writing.JsonOutput() - $codec.encodeValue(${ defaultValue.get.asExprOf[u] }, out) - Some(out.toString.asInstanceOf[RawJson]) - } - else Expr(None) - } - ) - } - case t: EnumRef[?] => '{ EnumSchema(${ Expr(t.values) }) } - case t: OptionRef[?] => - // Go ahead and gen schema for body of Option. Higher level (ie class) is resposible for tracking if a - // value is required or not... - t.optionParamType.refType match - case '[e] => - defaultValue - .map { dv => - val dve = dv.asExprOf[Option[e]] - '{ - $dve match - case Some(a) => ${ genSchema[e](quotes)(t.optionParamType.asInstanceOf[RTypeRef[e]], context, overrides, Some('{ a }.asTerm)) } - case None => ${ genSchema[e](quotes)(t.optionParamType.asInstanceOf[RTypeRef[e]], context, overrides, None) } - } - } - .getOrElse(genSchema[e](quotes)(t.optionParamType.asInstanceOf[RTypeRef[e]], context, overrides, None)) - case t: TryRef[?] => - // Go ahead and gen schema for body of Try. Higher level (ie class) is resposible for tracking if a - // value is required or not... - t.tryRef.refType match - case '[e] => - genSchema[e](quotes)(t.tryRef.asInstanceOf[RTypeRef[e]], context, overrides, None) - case t: SeqRef[?] => - t.refType match - case '[u] => - t.elementRef.refType match - case '[e] => - '{ - ArraySchema( - ${ genSchema[e](quotes)(t.elementRef.asInstanceOf[RTypeRef[e]], context, overrides, None) }, - ${ Expr(context.flatMap(_.annotations.get("minItems")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("maxItems")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("uniqueItems")).flatMap(_.get("value")).map(_.toBoolean)) }, - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - if defaultValue.isDefined then - val codec = JsonCodecMaker.generateCodecFor[u](t.asInstanceOf[RTypeRef[u]], SJConfig) - '{ - val out = new writing.JsonOutput() - $codec.encodeValue(${ defaultValue.get.asExprOf[u] }, out) - Some(out.result.asInstanceOf[RawJson]) - } - else Expr(None) - } - ) - } - case t: SetRef[?] => - t.refType match - case '[u] => - t.elementRef.refType match - case '[e] => - '{ - ArraySchema( - ${ genSchema[e](quotes)(t.elementRef.asInstanceOf[RTypeRef[e]], context, overrides, None) }, - ${ Expr(context.flatMap(_.annotations.get("minItems")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("maxItems")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("uniqueItems")).flatMap(_.get("value")).map(_.toBoolean)) }, - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - if defaultValue.isDefined then - val codec = JsonCodecMaker.generateCodecFor[u](t.asInstanceOf[RTypeRef[u]], SJConfig) - '{ - val out = new writing.JsonOutput() - $codec.encodeValue(${ defaultValue.get.asExprOf[u] }, out) - Some(out.result.asInstanceOf[RawJson]) - } - else Expr(None) - } - ) - } - case t: TupleRef[?] => - t.refType match - case '[u] => - '{ - TupleSchema( - ${ - Expr.ofList(t.tupleRefs.map { tr => - tr.refType match - case '[w] => - genSchema[w](quotes)(tr.asInstanceOf[RTypeRef[w]], context, overrides, None) - }) - }, - ${ Expr(context.flatMap(_.annotations.get("items")).flatMap(_.get("value")).map(_.toBoolean)) }, - ${ Expr(context.flatMap(_.annotations.get("minItems")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("maxItems")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("uniqueItems")).flatMap(_.get("value")).map(_.toBoolean)) }, - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - if defaultValue.isDefined then - val codec = JsonCodecMaker.generateCodecFor[u](t.asInstanceOf[RTypeRef[u]], SJConfig) - '{ - val out = new writing.JsonOutput() - $codec.encodeValue(${ defaultValue.get.asExprOf[u] }, out) - Some(out.result.asInstanceOf[RawJson]) - } - else Expr(None) - } - ) - } - case t: BooleanRef => - '{ - BooleanSchema( - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - ofOption(defaultValue.map(v => '{ ${ v.asExprOf[Boolean] }.toString.asInstanceOf[RawJson] })) - } - ) - } - case t: DoubleRef => - '{ - NumberSchema( - ${ Expr(context.flatMap(_.annotations.get("minimum")).flatMap(_.get("value")).map(_.toDouble)) }, - ${ Expr(context.flatMap(_.annotations.get("maximum")).flatMap(_.get("value")).map(_.toDouble)) }, - ${ Expr(context.flatMap(_.annotations.get("exclusiveMinimum")).flatMap(_.get("value")).map(_.toDouble)) }, - ${ Expr(context.flatMap(_.annotations.get("exclusiveMaximum")).flatMap(_.get("value")).map(_.toDouble)) }, - ${ Expr(context.flatMap(_.annotations.get("multipleOf")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - ofOption(defaultValue.map(v => '{ ${ v.asExprOf[Double] }.toString.asInstanceOf[RawJson] })) - } - ) - } - case t: IntRef => - '{ - IntegerSchema( - ${ Expr(context.flatMap(_.annotations.get("minimum")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("maximum")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("exclusiveMinimum")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("exclusiveMaximum")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("multipleOf")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - ofOption(defaultValue.map(v => '{ ${ v.asExprOf[Int] }.toString.asInstanceOf[RawJson] })) - } - ) - } - case t: LongRef => - '{ - IntegerSchema( - ${ Expr(context.flatMap(_.annotations.get("minimum")).flatMap(_.get("value")).map(_.toLong)) }, - ${ Expr(context.flatMap(_.annotations.get("maximum")).flatMap(_.get("value")).map(_.toLong)) }, - ${ Expr(context.flatMap(_.annotations.get("exclusiveMinimum")).flatMap(_.get("value")).map(_.toLong)) }, - ${ Expr(context.flatMap(_.annotations.get("exclusiveMaximum")).flatMap(_.get("value")).map(_.toLong)) }, - ${ Expr(context.flatMap(_.annotations.get("multipleOf")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - ofOption(defaultValue.map(v => '{ ${ v.asExprOf[Long] }.toString.asInstanceOf[RawJson] })) - } - ) - } - case t: StringRef => - '{ - StringSchema( - ${ Expr(context.flatMap(_.annotations.get("minLength")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("maxLength")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("pattern")).flatMap(_.get("value")).map(v => StringEscapeUtils.escapeJson(v))) }, - ${ Expr(context.flatMap(_.annotations.get("format")).flatMap(_.get("value"))) }.map(v => StringFormat.valueOf(v)), - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - ofOption(defaultValue.map(v => '{ ("\"" + StringEscapeUtils.escapeJson(${ v.asExprOf[String] }) + "\"").asInstanceOf[RawJson] })) - } - ) - } - case t: ZonedDateTimeRef => - '{ - StringSchema( - ${ Expr(context.flatMap(_.annotations.get("minLength")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("maxLength")).flatMap(_.get("value")).map(_.toInt)) }, - ${ Expr(context.flatMap(_.annotations.get("pattern")).flatMap(_.get("value")).map(v => StringEscapeUtils.escapeJson(v))) }, - Some(StringFormat.`date-time`), - ${ Expr(context.flatMap(_.annotations.get("description")).flatMap(_.get("value"))) }, - ${ - ofOption(defaultValue.map(v => '{ ${ v.asExprOf[java.time.ZonedDateTime] }.toString.asInstanceOf[RawJson] })) - } - ) - } - case t: ScalaClassRef[?] => - t.refType match - case '[u] => - val requiredFields = Expr(t.fields.collect { - case f: FieldInfoRef if !f.fieldRef.isInstanceOf[OptionRef[?]] => f.name - }) - '{ - ObjectSchema( - ${ - Expr.ofList( - t.fields.map(f => - f.fieldRef.refType match - case '[b] => - // Get default value if any - val tpe = TypeRepr.of[u] - val classCompanion = tpe.typeSymbol.companionClass - val companionModule = tpe.typeSymbol.companionModule - val dvMembers = classCompanion.methodMember("$lessinit$greater$default$" + (f.index + 1)) - val fieldDefault = - if dvMembers.isEmpty then None - else - val methodSymbol = dvMembers.head - val dvSelectNoTArgs = Ref(companionModule).select(methodSymbol) - val dvSelect = methodSymbol.paramSymss match - case Nil => dvSelectNoTArgs - case List(params) if (params.exists(_.isTypeParam)) => - typeArgs(tpe) match - case Nil => ??? // throw JsonParseError("Expected an applied type", ???) - case typeArgs => TypeApply(dvSelectNoTArgs, typeArgs.map(Inferred(_))) - case _ => ??? // fail(s"Default method for ${symbol.name} of class ${tpe.show} have a complex " + - Some(dvSelect) - Expr.ofTuple((Expr(f.name), genSchema[b](quotes)(f.fieldRef.asInstanceOf[RTypeRef[b]], Some(f.asInstanceOf[ScalaFieldInfoRef]), overrides, fieldDefault))) - ) - ) - }.toMap, - $requiredFields, - ${ Expr(t.annotations.get("co.blocke.scalajack.schema.additionalProperties").flatMap(_.get("value")).map(_.toBoolean)) }, - ${ - if isInitialSchema then '{ Some(new URL("http://jsons-schema.org/draft-04/schema#")) } - else '{ None } - }, - ${ - if isInitialSchema then Expr(t.annotations.get("co.blocke.scalajack.schema.id").flatMap(_.get("value"))) - else '{ None } - }, - ${ - if isInitialSchema then Expr(t.annotations.get("co.blocke.scalajack.schema.title").flatMap(_.get("value"))) - else '{ None } - }, - ${ Expr(t.annotations.get("co.blocke.scalajack.schema.description").flatMap(_.get("value"))) }, - ${ - if defaultValue.isDefined then - val codec = JsonCodecMaker.generateCodecFor[u](t.asInstanceOf[RTypeRef[u]], SJConfig.suppressTypeHints()) - '{ - val out = new writing.JsonOutput() - $codec.encodeValue(${ defaultValue.get.asExprOf[u] }, out) - Some(out.result.asInstanceOf[RawJson]) - } - else Expr(None) - } - ) - } - case t: TraitRef[?] => - t.refType match - case '[u] => - if t.childrenAreObject then '{ EnumSchema(${ Expr(t.sealedChildren.map(_.name.split("\\.").last)) }) } - else throw new Exception(s"Unsupported type ${rt.name} of type ${rt.getClass.getName} for JSON schema generation") - case x => throw new Exception(s"Unsupported type ${rt.name} of type ${rt.getClass.getName} for JSON schema generation") - }) - } diff --git a/src/main/scala/co.blocke.scalajack/json/schema/Schema.scala b/src/main/scala/co.blocke.scalajack/json/schema/Schema.scala deleted file mode 100644 index 0ae149cb..00000000 --- a/src/main/scala/co.blocke.scalajack/json/schema/Schema.scala +++ /dev/null @@ -1,114 +0,0 @@ -package co.blocke.scalajack -package json -package schema - -import java.net.URL - -/** A *very* sparse implementation of JSON Schema Draft 4 (model only). It is full of holes, but is just - * enough for what I needed at the time--and had the advantage of leveraging scala-reflection - * to generate the schema from a type. At the time of this writing, no other Scala 3-capable JSON - * library generated a JSON Schema document. (ZIO-Json genrated their own Schema structure, however.) - * - * If there is strong utility for a full-blown JSON Schema utility, that might be something I look - * at later, unless someone wants to take it up. I would suggest development of "helper" objects - * for the more advanced schema operaitons (allOf, anyOf, if/then/else, etc) vs trying to do all that - * in annotations. - */ - -// Reference: https://json-schema.org/UnderstandingJSONSchema.pdf - -enum SchemaType: - case `null`, `boolean`, `object`, `array`, `string`, `number`, `integer` - -enum StringFormat: - case `date-time`, email, hostname, ipv4, ipv6, uuid, uri, url - -// opaque type JSON_LITERAL = String - -type Schema = StdSchema | EnumSchema - -sealed trait StdSchema: - val `type`: SchemaType - val description: Option[String] - val default: Option[RawJson] - -// Formats: Dates & Times, Email, Hostnames -case class StringSchema( - minLength: Option[Int] = None, - maxLength: Option[Int] = None, - pattern: Option[String] = None, - format: Option[StringFormat] = None, - description: Option[String] = None, - default: Option[RawJson] = None, - `type`: SchemaType = SchemaType.`string` -) extends StdSchema - -case class IntegerSchema( - minimum: Option[Long] = None, - maximum: Option[Long] = None, - exclusiveMinimum: Option[Long] = None, - exclusiveMaximum: Option[Long] = None, - multipleOf: Option[Int] = None, - description: Option[String] = None, - default: Option[RawJson] = None, - `type`: SchemaType = SchemaType.`integer` -) extends StdSchema - -case class NumberSchema( - minimum: Option[Double] = None, - maximum: Option[Double] = None, - exclusiveMinimum: Option[Double] = None, - exclusiveMaximum: Option[Double] = None, - multipleOf: Option[Int] = None, - description: Option[String] = None, - default: Option[RawJson] = None, - `type`: SchemaType = SchemaType.`number` -) extends StdSchema - -case class BooleanSchema( - description: Option[String] = None, - default: Option[RawJson] = None, - `type`: SchemaType = SchemaType.`boolean` -) extends StdSchema - -case class NullSchema(description: Option[String] = None, `type`: SchemaType = SchemaType.`null`) extends StdSchema: - val default: Option[RawJson] = None // no default for null possible - -case class ArraySchema( - items: Schema, - minItems: Option[Int] = None, - maxItems: Option[Int] = None, - uniqueItems: Option[Boolean] = None, - description: Option[String] = None, - default: Option[RawJson] = None, - `type`: SchemaType = SchemaType.`array` -) extends StdSchema - -case class TupleSchema( - prefixItems: List[Schema], - items: Option[Boolean] = None, - minItems: Option[Int] = None, - maxItems: Option[Int] = None, - uniqueItems: Option[Boolean] = None, - description: Option[String] = None, - default: Option[RawJson] = None, - `type`: SchemaType = SchemaType.`array` -) extends StdSchema - -// Note: patternProperties not implemented at this time (I didn't need them) -case class ObjectSchema( - properties: Map[String, Schema], - required: List[String], - additionalProperties: Option[Boolean] = None, - `$schema`: Option[URL] = Some(new URL("http://jsons-schema.org/draft-04/schema#")), - `$id`: Option[String] = None, - title: Option[String] = None, - description: Option[String] = None, - default: Option[RawJson] = None, - `type`: SchemaType = SchemaType.`object` -) extends StdSchema - -// Weird exception to other schemas--no type, or other decorating feature... just the enum values -case class EnumSchema( - `enum`: List[String] -) diff --git a/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala b/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala index 108c39ef..b1417626 100644 --- a/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala +++ b/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala @@ -245,11 +245,6 @@ case class JsonOutput(): internal.append('"') comma = true - inline def valueRaw(v: RawJson): Unit = - maybeComma() - internal.append(v.asInstanceOf[String]) - comma = true - inline def value(v: java.lang.Boolean): Unit = maybeComma() if v == null then internal.append("null") diff --git a/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala index a9e89727..3e1ad9da 100644 --- a/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala @@ -155,12 +155,134 @@ class MapSpec() extends AnyFunSpec with JsonMatchers: js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") sj.fromJson(js) shouldEqual (inst) } - it("Map value must work - SeqMap (examplar for all other immutable Maps)") { + it("Map value must work - SeqMap") { val inst = MapHolder3[String, Distance](scala.collection.immutable.SeqMap("w" -> new Distance(1.23), "y" -> Distance(4.56))) val sj = sjCodecOf[MapHolder3[String, Distance]] val js = sj.toJson(inst) js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") sj.fromJson(js) shouldEqual (inst) } + it("Map value must work - VectorMap (examplar for all other immutable Maps)") { + val inst = MapHolder4[String, Distance](scala.collection.immutable.TreeMap("w" -> new Distance(1.23), "y" -> Distance(4.56))) + val sj = sjCodecOf[MapHolder4[String, Distance]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map keys of remaining types must work (test coverage addition)") { + val inst = MapHolder[java.lang.Number, Int](Map(java.lang.Integer.valueOf(5).asInstanceOf[java.lang.Number] -> 12)) + val sj = sjCodecOf[MapHolder[java.lang.Number, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"5":12}}""") + sj.fromJson(js) shouldEqual (inst) + + val inst2 = MapHolder[java.lang.Short, Int](Map(java.lang.Short.valueOf("5") -> 12)) + val sj2 = sjCodecOf[MapHolder[java.lang.Short, Int]] + val js2 = sj2.toJson(inst2) + js2 should matchJson("""{"a":{"5":12}}""") + sj2.fromJson(js2) shouldEqual (inst2) + + val inst3 = MapHolder[java.lang.Long, Int](Map(java.lang.Long.valueOf(5) -> 12)) + val sj3 = sjCodecOf[MapHolder[java.lang.Long, Int]] + val js3 = sj3.toJson(inst3) + js3 should matchJson("""{"a":{"5":12}}""") + sj3.fromJson(js3) shouldEqual (inst3) + + val inst4 = MapHolder[java.lang.Integer, Int](Map(java.lang.Integer.valueOf(5) -> 12)) + val sj4 = sjCodecOf[MapHolder[java.lang.Integer, Int]] + val js4 = sj4.toJson(inst4) + js4 should matchJson("""{"a":{"5",12}}""") + sj4.fromJson(js4) shouldEqual (inst4) + + val inst5 = MapHolder[java.lang.Float, Int](Map(java.lang.Float.valueOf(5) -> 12)) + val sj5 = sjCodecOf[MapHolder[java.lang.Float, Int]] + val js5 = sj5.toJson(inst5) + js5 should matchJson("""{"a":{"5.0":12}}""") + sj5.fromJson(js5) shouldEqual (inst5) + + val inst6 = MapHolder[java.lang.Double, Int](Map(java.lang.Double.valueOf(5) -> 12)) + val sj6 = sjCodecOf[MapHolder[java.lang.Double, Int]] + val js6 = sj6.toJson(inst6) + js6 should matchJson("""{"a":{"5.0":12}}""") + sj6.fromJson(js6) shouldEqual (inst6) + + val inst7 = MapHolder[java.lang.Byte, Int](Map(java.lang.Byte.valueOf("5") -> 12)) + val sj7 = sjCodecOf[MapHolder[java.lang.Byte, Int]] + val js7 = sj7.toJson(inst7) + js7 should matchJson("""{"a":{"5":12}}""") + sj7.fromJson(js7) shouldEqual (inst7) + + val inst8 = MapHolder[java.math.BigDecimal, Int](Map(java.math.BigDecimal.valueOf(5) -> 12)) + val sj8 = sjCodecOf[MapHolder[java.math.BigDecimal, Int]] + val js8 = sj8.toJson(inst8) + js8 should matchJson("""{"a":{"5":12}}""") + sj8.fromJson(js8) shouldEqual (inst8) + + val inst9 = MapHolder[java.math.BigInteger, Int](Map(java.math.BigInteger.valueOf(5) -> 12)) + val sj9 = sjCodecOf[MapHolder[java.math.BigInteger, Int]] + val js9 = sj9.toJson(inst9) + js9 should matchJson("""{"a":{"5":12}}""") + sj9.fromJson(js9) shouldEqual (inst9) + + val inst10 = MapHolder[java.lang.Boolean, Int](Map(java.lang.Boolean.valueOf(true) -> 12)) + val sj10 = sjCodecOf[MapHolder[java.lang.Boolean, Int]] + val js10 = sj10.toJson(inst10) + js10 should matchJson("""{"a":{"true":12}}""") + sj10.fromJson(js10) shouldEqual (inst10) + + val inst11 = MapHolder[Short, Int](Map(5.toShort -> 12)) + val sj11 = sjCodecOf[MapHolder[Short, Int]] + val js11 = sj11.toJson(inst11) + js11 should matchJson("""{"a":{"5":12}}""") + sj11.fromJson(js11) shouldEqual (inst11) + + val inst12 = MapHolder[Byte, Int](Map(5.toByte -> 12)) + val sj12 = sjCodecOf[MapHolder[Byte, Int]] + val js12 = sj12.toJson(inst12) + js12 should matchJson("""{"a":{"5":12}}""") + sj12.fromJson(js12) shouldEqual (inst12) + + val inst13 = MapHolder[Float, Int](Map(5.0.toFloat -> 12)) + val sj13 = sjCodecOf[MapHolder[Float, Int]] + val js13 = sj13.toJson(inst13) + js13 should matchJson("""{"a":{"5.0":12}}""") + sj13.fromJson(js13) shouldEqual (inst13) + + val inst14 = MapHolder[scala.math.BigDecimal, Int](Map(scala.math.BigDecimal(5) -> 12)) + val sj14 = sjCodecOf[MapHolder[scala.math.BigDecimal, Int]] + val js14 = sj14.toJson(inst14) + js14 should matchJson("""{"a":{"5":12}}""") + sj14.fromJson(js14) shouldEqual (inst14) + + val inst15 = MapHolder[scala.math.BigInt, Int](Map(scala.math.BigInt(5) -> 12)) + val sj15 = sjCodecOf[MapHolder[scala.math.BigInt, Int]] + val js15 = sj15.toJson(inst15) + js15 should matchJson("""{"a":{"5":12}}""") + sj15.fromJson(js15) shouldEqual (inst15) + + val inst16 = MapHolder[OnOff, OnOff](Map(true.asInstanceOf[OnOff] -> false.asInstanceOf[OnOff])) + val sj16 = sjCodecOf[MapHolder[OnOff, OnOff]] + val js16 = sj16.toJson(inst16) + js16 should matchJson("""{"a":{"true":false}}""") + sj16.fromJson(js16) shouldEqual (inst16) + + val inst17 = MapHolder[Boolean, OnOff](Map(true -> false.asInstanceOf[OnOff])) + val sj17 = sjCodecOf[MapHolder[Boolean, OnOff]] + val js17 = sj17.toJson(inst17) + js17 should matchJson("""{"a":{"true":false}}""") + sj17.fromJson(js17) shouldEqual (inst17) + + val inst18 = MapHolder[OnOff, Boolean](Map(true.asInstanceOf[OnOff] -> false)) + val sj18 = sjCodecOf[MapHolder[OnOff, Boolean]] + val js18 = sj18.toJson(inst18) + js18 should matchJson("""{"a":{"true":false}}""") + sj18.fromJson(js18) shouldEqual (inst18) + + val now = java.time.Instant.now() + val inst19 = MapHolder[java.time.Instant, Boolean](Map(now -> false)) + val sj19 = sjCodecOf[MapHolder[java.time.Instant, Boolean]] + val js19 = sj19.toJson(inst19) + sj19.fromJson(js19) shouldEqual (inst19) + } } } diff --git a/src/test/scala/co.blocke.scalajack/json/collections/Model.scala b/src/test/scala/co.blocke.scalajack/json/collections/Model.scala index b097c90c..9ffc6e79 100644 --- a/src/test/scala/co.blocke.scalajack/json/collections/Model.scala +++ b/src/test/scala/co.blocke.scalajack/json/collections/Model.scala @@ -11,12 +11,18 @@ opaque type Counter = Short case class SeqHolder[T](a: Seq[T]) case class SetHolder[T](a: Set[T]) +case class MSeqHolder[T](a: scala.collection.mutable.Seq[T]) +case class MSetHolder[T](a: scala.collection.mutable.Set[T]) +case class VectorHolder[T](a: Vector[T]) +case class IndexedSeqHolder[T](a: IndexedSeq[T]) +case class IterableHolder[T](a: Iterable[T]) case class ArrayHolder[T](a: Array[T]) case class Holder[T](a: T) case class MapHolder[T, V](a: Map[T, V]) case class MapHolder2[T, V](a: scala.collection.immutable.HashMap[T, V]) // specific -case class MapHolder3[T, V](a: scala.collection.immutable.SeqMap[T, V]) // open coersion +case class MapHolder3[T, V](a: scala.collection.immutable.SeqMap[T, V]) // specific +case class MapHolder4[T, V](a: scala.collection.immutable.TreeMap[T, V]) // open coersion case class MMapHolder[T, V](a: scala.collection.mutable.Map[T, V]) // specific case class MMapHolder2[T, V](a: scala.collection.mutable.HashMap[T, V]) // specific case class MMapHolder3[T, V](a: scala.collection.mutable.SeqMap[T, V]) // open coersion @@ -25,6 +31,7 @@ case class JMapHolder[T, V](a: JMap[T, V]) class Distance(val meters: Double) extends AnyVal case class TupleHolder[A, B, C](a: (A, B, C)) +case class TupleOneHolder[A](a: Tuple1[A]) case class ArrayListHolder[T](a: ArrayList[T]) case class JSetHolder[T](a: JSet[T]) diff --git a/src/test/scala/co.blocke.scalajack/json/collections/SeqSetArraySpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/SeqSetArraySpec.scala index 0313b347..a405b245 100644 --- a/src/test/scala/co.blocke.scalajack/json/collections/SeqSetArraySpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/collections/SeqSetArraySpec.scala @@ -36,6 +36,13 @@ class SeqSetArraySpec() extends AnyFunSpec with JsonMatchers: js should matchJson("""{"a":["a","b","c"]}""") sj.fromJson(js) shouldEqual (inst) } + it("Mutable Seq of string must work") { + val inst = MSeqHolder[String](scala.collection.mutable.ListBuffer("a", "b", "c")) + val sj = sjCodecOf[MSeqHolder[String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["a","b","c"]}""") + sj.fromJson(js) shouldEqual (inst) + } it("Seq of boolean must work") { val inst = SeqHolder[Boolean](List(true, false, true)) val sj = sjCodecOf[SeqHolder[Boolean]] @@ -107,6 +114,13 @@ class SeqSetArraySpec() extends AnyFunSpec with JsonMatchers: js should matchJson("""{"a":["a","b","c"]}""") sj.fromJson(js) shouldEqual (inst) } + it("Mutable Set of string must work") { + val inst = MSetHolder[String](scala.collection.mutable.HashSet("a", "b", "c")) + val sj = sjCodecOf[MSetHolder[String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["a","b","c"]}""") + sj.fromJson(js) shouldEqual (inst) + } it("Set of boolean must work") { val inst = SetHolder[Boolean](HashSet(true, false, true)) val sj = sjCodecOf[SetHolder[Boolean]] @@ -227,5 +241,28 @@ class SeqSetArraySpec() extends AnyFunSpec with JsonMatchers: js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") sj.fromJson(js).a.toList shouldEqual (inst.a.toList) } + + it("Vector of class must work") { + val inst = VectorHolder[Person](Vector(Person("Bob", 35), Person("Sally", 54))) + val sj = sjCodecOf[VectorHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("IndexedSeq of class must work") { + val inst = IndexedSeqHolder[Person](IndexedSeq(Person("Bob", 35), Person("Sally", 54))) + val sj = sjCodecOf[IndexedSeqHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("Iterable of class must work") { + val inst = IterableHolder[Person](Seq(Person("Bob", 35), Person("Sally", 54))) + val sj = sjCodecOf[IterableHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + } } diff --git a/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala index b74f8045..bbc2947e 100644 --- a/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala @@ -36,6 +36,13 @@ class TupleSpec() extends AnyFunSpec with JsonMatchers: js should matchJson("""{"a":[[1,2],{"a":3,"b":4},[1.23,"X",true]]}""") sj.fromJson(js) shouldEqual inst } + it("Tuple of one element must work") { + val inst = TupleOneHolder[Int](Tuple1(15)) + val sj = sjCodecOf[TupleOneHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[15]}""") + sj.fromJson(js) shouldEqual inst + } } describe(colorString("--- Negative Tests ---")) { diff --git a/src/test/scala/co.blocke.scalajack/json/misc/Model.scala b/src/test/scala/co.blocke.scalajack/json/misc/Model.scala index 043756ab..4e27fa5b 100644 --- a/src/test/scala/co.blocke.scalajack/json/misc/Model.scala +++ b/src/test/scala/co.blocke.scalajack/json/misc/Model.scala @@ -34,6 +34,7 @@ case class OptionalHolder[T]( j: Either[Optional[T], T] // Either of Optional (L) ) +case class SimpleOptionHolder[T](a: Option[T]) case class TryHolder[T](a: Try[T]) case class TryHolder2[T](a: Seq[Try[T]], b: (Try[T], Try[T])) diff --git a/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala index 3fa16ac6..4d0171b9 100644 --- a/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala @@ -176,5 +176,12 @@ class OptionSpec() extends AnyFunSpec with JsonMatchers: js should matchJson("""{"a":null}""") sj.fromJson(js) shouldEqual (EitherRecipeJ[Int](null)) } + it("Option of Either should work") { + val inst = SimpleOptionHolder[Either[Boolean, Int]](Some(Right(5))) + val sj = sjCodecOf[SimpleOptionHolder[Either[Boolean, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5}""") + sj.fromJson(js) shouldEqual (inst) + } } } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scala index df1c1fcf..c3f59374 100644 --- a/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scala @@ -52,6 +52,13 @@ class TrySpec() extends AnyFunSpec with JsonMatchers: js should matchJson("""{}""") sj.fromJson(js) shouldEqual (inst) } + it("Try of Either must work (Success)") { + val inst = TryHolder[Either[Boolean, Int]](Success(Right(5))) + val sj = sjCodecOf[TryHolder[Either[Boolean, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5}""") + sj.fromJson(js) shouldEqual (inst) + } it("Try w/policy AS_NULL must work (Failure)") { val inst = TryHolder[Int](Failure(new Exception("boom"))) val sj = sjCodecOf[TryHolder[Int]](SJConfig.withTryFailureHandling(TryPolicy.AS_NULL)) diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala b/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala index 49ede9e6..66ae8799 100644 --- a/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala @@ -119,6 +119,56 @@ class ScalaPrimSpec() extends AnyFunSpec with JsonMatchers: js should matchJson("""{"s1":"something\b\n\f\r\t\""" + """u2606","s2":"","s3":null}""") sj.fromJson(js) shouldEqual inst } + + it("Any type for all primitives must work") { + val sj = sjCodecOf[AnyShell] + val prims: List[(Any, String, Option[Any => String])] = List( + (null, """{"a":null}""", None), + (scala.math.BigDecimal(5), """{"a":5}""", None), + (scala.math.BigInt(5), """{"a":5}""", None), + (true, """{"a":true}""", None), + (5.toByte, """{"a":5}""", None), + ('x', """{"a":"x"}""", Some((c: Any) => c.toString)), + (5.0, """{"a":5.0}""", None), + (5.0.toFloat, """{"a":5.0}""", None), + (5, """{"a":5}""", None), + (5L, """{"a":5}""", None), + (5.toShort, """{"a":5}""", None), + ("foo", """{"a":"foo"}""", None), + (java.lang.Boolean.valueOf(true), """{"a":true}""", None), + (java.lang.Byte.valueOf(5.toByte), """{"a":5}""", None), + (java.lang.Character.valueOf('x'), """{"a":"x"}""", Some((c: Any) => c.toString)), + (java.lang.Double.valueOf(5.0), """{"a":5.0}""", None), + (java.lang.Float.valueOf(5.0.toFloat), """{"a":5.0}""", None), + (java.lang.Integer.valueOf(5), """{"a":5}""", None), + (java.lang.Long.valueOf(5), """{"a":5}""", None), + (java.lang.Short.valueOf(5.toShort), """{"a":5}""", None), + (java.lang.Integer.valueOf(5).asInstanceOf[java.lang.Number], """{"a":5}""", None), + (java.time.Duration.ofHours(5), """{"a":"PT5H"}""", Some((c: Any) => c.toString)), + (java.time.Instant.ofEpochSecond(1234567), """{"a":"1970-01-15T06:56:07Z"}""", Some((c: Any) => c.toString)), + (java.time.LocalDate.of(2024, 3, 15), """{"a":"2024-03-15"}""", Some((c: Any) => c.toString)), + (java.time.LocalDateTime.of(2024, 3, 15, 4, 15, 3), """{"a":"2024-03-15T04:15:03"}""", Some((c: Any) => c.toString)), + (java.time.LocalTime.of(4, 15, 3), """{"a":"04:15:03"}""", Some((c: Any) => c.toString)), + (java.time.MonthDay.of(12, 25), """{"a":"--12-25"}""", Some((c: Any) => c.toString)), + (java.time.OffsetDateTime.of(2024, 3, 15, 9, 15, 1, 0, java.time.ZoneOffset.ofHours(5)), """{"a":"2024-03-15T09:15:01+05:00"}""", Some((c: Any) => c.toString)), + (java.time.OffsetTime.of(9, 15, 1, 0, java.time.ZoneOffset.ofHours(5)), """{"a":"09:15:01+05:00"}""", Some((c: Any) => c.toString)), + (java.time.Period.ofDays(5), """{"a":"P5D"}""", Some((c: Any) => c.toString)), + (java.time.Year.of(2024), """{"a":"2024"}""", Some((c: Any) => c.toString)), + (java.time.YearMonth.of(2024, 3), """{"a":"2024-03"}""", Some((c: Any) => c.toString)), + (java.time.ZoneOffset.ofHours(5), """{"a":"+05:00"}""", Some((c: Any) => c.toString)), + (java.time.ZonedDateTime.parse("2007-12-03T10:15:30+01:00"), """{"a":"2007-12-03T10:15:30+01:00"}""", Some((c: Any) => c.toString)), + (java.time.ZoneId.of("GMT+2"), """{"a":"GMT+02:00"}""", Some((c: Any) => c.toString)) + ) + prims.map { case (v, j, fn) => + val inst = AnyShell(v) + val js = sj.toJson(inst) + js should matchJson(j) + fn match { + case Some(f) => sj.fromJson(js) shouldEqual (AnyShell(f(v))) + case None => sj.fromJson(js) shouldEqual inst + } + } + } } // -------------------------------------------------------- diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/SimpleSpec.scala b/src/test/scala/co.blocke.scalajack/json/primitives/SimpleSpec.scala index d47e767d..b2b7452f 100644 --- a/src/test/scala/co.blocke.scalajack/json/primitives/SimpleSpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/primitives/SimpleSpec.scala @@ -166,7 +166,7 @@ class SimpleSpec() extends AnyFunSpec with JsonMatchers: it("Net types URL and URI must work") { val inst = SampleNet( null, - new URL("https://www.foom.com"), + new URI("https://www.foom.com").toURL(), null, new URI("https://www.foom.com") ) @@ -360,7 +360,7 @@ class SimpleSpec() extends AnyFunSpec with JsonMatchers: val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleNet].fromJson(js)) ex.show shouldEqual msg val js2 = """{"u1":null,"u2":"httpwww.foom.com","u3":null,"u4":"https://www.foom.com"}""" - the[java.net.MalformedURLException] thrownBy sjCodecOf[SampleNet].fromJson(js2) should have message """no protocol: httpwww.foom.com""" + the[java.lang.IllegalArgumentException] thrownBy sjCodecOf[SampleNet].fromJson(js2) should have message """URI is not absolute""" } it("UUID must break") {