From aa0763f0f343a217a7726a0b957e5dd657d624fb Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 19 Sep 2022 10:40:11 +0200 Subject: [PATCH] Update dependencies, add Scala Native support for circe integration, add jsoniter-scala integration --- build.sbt | 25 ++- .../geoscala/CirceDecodingTests.scala | 5 + .../free2move/geoscala/jsoniter_scala.scala | 152 ++++++++++++++++++ .../geoscala/JsoniterScalaDecodingTests.scala | 89 ++++++++++ project/plugins.sbt | 5 +- 5 files changed, 268 insertions(+), 8 deletions(-) create mode 100644 jsoniter-scala/src/main/scala/com/free2move/geoscala/jsoniter_scala.scala create mode 100644 jsoniter-scala/src/test/scala/com/free2move/geoscala/JsoniterScalaDecodingTests.scala diff --git a/build.sbt b/build.sbt index 5c0073f..6a53d7c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ -val scala212 = "2.12.16" +val scala212 = "2.12.17" val scala213 = "2.13.8" -val scala3 = "3.1.3" +val scala3 = "3.2.0" ThisBuild / tlBaseVersion := "0.2" @@ -17,11 +17,11 @@ ThisBuild / tlSonatypeUseLegacyHost := true ThisBuild / crossScalaVersions := Seq(scala212, scala213, scala3) ThisBuild / scalaVersion := scala213 // the default Scala -lazy val root = tlCrossRootProject.aggregate(core, circe, polyline).settings(name := "geo-scala") +lazy val root = tlCrossRootProject.aggregate(core, circe, jsoniterScala, polyline).settings(name := "geo-scala") lazy val commonSettings = Seq( libraryDependencies ++= Seq( - "org.scalatest" %%% "scalatest" % "3.2.12" % Test, + "org.scalatest" %%% "scalatest" % "3.2.13" % Test, "org.scalatestplus" %%% "scalacheck-1-16" % "3.2.13.0" % Test, "org.scalacheck" %%% "scalacheck" % "1.16.0" % Test ), @@ -39,8 +39,8 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) ) ) -val circeVersion = "0.14.2" -lazy val circe = crossProject(JVMPlatform, JSPlatform) +val circeVersion = "0.14.3" +lazy val circe = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("circe")) .dependsOn(core) @@ -52,6 +52,19 @@ lazy val circe = crossProject(JVMPlatform, JSPlatform) ) ) +val jsoniterScalaVersion = "2.17.4" +lazy val jsoniterScala = crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .in(file("jsoniter-scala")) + .dependsOn(core) + .settings( + commonSettings ++ Seq( + name := "geo-scala-jsoniter-scala", + libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % jsoniterScalaVersion, + libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % jsoniterScalaVersion % Provided + ) + ) + lazy val polyline = crossProject(JVMPlatform, JSPlatform, NativePlatform) .in(file("polyline")) .dependsOn(core) diff --git a/circe/src/test/scala/com/free2move/geoscala/CirceDecodingTests.scala b/circe/src/test/scala/com/free2move/geoscala/CirceDecodingTests.scala index f674043..df979da 100644 --- a/circe/src/test/scala/com/free2move/geoscala/CirceDecodingTests.scala +++ b/circe/src/test/scala/com/free2move/geoscala/CirceDecodingTests.scala @@ -79,6 +79,11 @@ class CirceDecodingTests extends AnyFlatSpec with Matchers with EitherValues { List(Feature(Json.obj("id" := 7), Point(Coordinate(12.3046875, 51.8357775)))) ) ) + parser.decode[GeoJson[Json]](json) shouldBe Right( + FeatureCollection( + List(Feature(Json.obj("id" := 7), Point(Coordinate(12.3046875, 51.8357775)))) + ) + ) } } diff --git a/jsoniter-scala/src/main/scala/com/free2move/geoscala/jsoniter_scala.scala b/jsoniter-scala/src/main/scala/com/free2move/geoscala/jsoniter_scala.scala new file mode 100644 index 0000000..284641e --- /dev/null +++ b/jsoniter-scala/src/main/scala/com/free2move/geoscala/jsoniter_scala.scala @@ -0,0 +1,152 @@ +/* + * Copyright 2019 GHM Mobile Development GmbH + * + * 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 com.free2move.geoscala + +import com.github.plokhotnyuk.jsoniter_scala.core._ +import com.github.plokhotnyuk.jsoniter_scala.macros._ + +object jsoniter_scala { + // Uncomment for printing of generating codecs + // implicit val printCodec: CodecMakerConfig.PrintCodec = new CodecMakerConfig.PrintCodec {} + + implicit val coordinateCodec: JsonValueCodec[Coordinate] = + new JsonValueCodec[Coordinate] { + override def decodeValue(in: JsonReader, default: Coordinate): Coordinate = + if (in.isNextToken('[')) { + val lon = in.readDouble() + if (!in.isNextToken(',')) in.commaError() + val lat = in.readDouble() + while (in.isNextToken(',')) in.skip() + if (!in.isCurrentToken(']')) in.arrayEndOrCommaError() + Coordinate(lon, lat) + } else in.readNullOrTokenError(default, '[') + + override def encodeValue(x: Coordinate, out: JsonWriter): Unit = { + out.writeArrayStart() + out.writeVal(x.longitude) + out.writeVal(x.latitude) + out.writeArrayEnd() + } + + override def nullValue: Coordinate = null + } + + implicit val listOfCoordinatesCodec: JsonValueCodec[List[Coordinate]] = + JsonCodecMaker.make + + implicit val listOfListOfCoordinatesCodec: JsonValueCodec[List[List[Coordinate]]] = + JsonCodecMaker.make + + implicit val listOfListOfListOfCoordinatesCodec: JsonValueCodec[List[List[List[Coordinate]]]] = + JsonCodecMaker.make + + implicit val pointCodec: JsonValueCodec[Point] = + makeGeometryCodec("Point", _.coordinates, Point) + + implicit val multiPointCodec: JsonValueCodec[MultiPoint] = + makeGeometryCodec("MultiPoint", _.coordinates, MultiPoint) + + implicit val lineStringCodec: JsonValueCodec[LineString] = + makeGeometryCodec("LineString", _.coordinates, LineString) + + implicit val multiLineStringCodec: JsonValueCodec[MultiLineString] = + makeGeometryCodec("MultiLineString", _.coordinates, MultiLineString) + + implicit val polygonCodec: JsonValueCodec[Polygon] = + makeGeometryCodec("Polygon", _.coordinates, Polygon) + + implicit val multiPolygonCodec: JsonValueCodec[MultiPolygon] = + makeGeometryCodec("MultiPolygon", _.coordinates, MultiPolygon) + + private def makeGeometryCodec[C: JsonValueCodec, G <: Geometry](`type`: String, coords: G => C, geom: C => G): JsonValueCodec[G] = + new JsonValueCodec[G] { + private val coordinatesCodec: JsonValueCodec[C] = implicitly[JsonValueCodec[C]] + + override def decodeValue(in: JsonReader, default: G): G = + if (in.isNextToken('{')) { + var coordinates: C = coordinatesCodec.nullValue + var mask = 3 + var len = -1 + while (len < 0 || in.isNextToken(',')) { + len = in.readKeyAsCharBuf() + if (in.isCharBufEqualsTo(len, "type")) { + if ((mask & 0x1) != 0) mask ^= 0x1 + else in.duplicatedKeyError(len) + len = in.readStringAsCharBuf() + if (!in.isCharBufEqualsTo(len, `type`)) in.discriminatorValueError("type") + } else if (in.isCharBufEqualsTo(len, "coordinates")) { + if ((mask & 0x2) != 0) mask ^= 0x2 + else in.duplicatedKeyError(len) + coordinates = coordinatesCodec.decodeValue(in, coordinates) + } else in.skip() + } + geom(coordinates) + } else in.readNullOrTokenError(default, '}') + + override def encodeValue(x: G, out: JsonWriter): Unit = { + out.writeObjectStart() + out.writeNonEscapedAsciiKey("type") + out.writeNonEscapedAsciiVal(`type`) + out.writeNonEscapedAsciiKey("coordinates") + coordinatesCodec.encodeValue(coords(x), out) + out.writeObjectEnd() + } + + override def nullValue: G = null.asInstanceOf[G] + } + + implicit val geometryCodec: JsonValueCodec[Geometry] = + JsonCodecMaker.make + + implicit def featureCodec[P: JsonValueCodec]: JsonValueCodec[Feature[P]] = + JsonCodecMaker.make + + implicit def featureCollectionCodec[P: JsonValueCodec]: JsonValueCodec[FeatureCollection[P]] = + JsonCodecMaker.make + + implicit def geoJson[P: JsonValueCodec]: JsonValueCodec[GeoJson[P]] = + new JsonValueCodec[GeoJson[P]] { + private val fc: JsonValueCodec[Feature[P]] = featureCodec + private val fcc: JsonValueCodec[FeatureCollection[P]] = featureCollectionCodec + + override def decodeValue(in: JsonReader, default: GeoJson[P]): GeoJson[P] = { + in.setMark() + if (in.isNextToken('{')) { + if (!in.skipToKey("type")) in.discriminatorError() + val len = in.readStringAsCharBuf() + in.rollbackToMark() + if (in.isCharBufEqualsTo(len, "Feature")) fc.decodeValue(in, fc.nullValue) + else if (in.isCharBufEqualsTo(len, "FeatureCollection")) fcc.decodeValue(in, fcc.nullValue) + else geometryCodec.decodeValue(in, geometryCodec.nullValue).asInstanceOf[GeoJson[P]] + } else { + val gj = in.readNullOrTokenError(default, '{') + in.rollbackToMark() + in.skip() + gj + } + } + + override def encodeValue(x: GeoJson[P], out: JsonWriter): Unit = + x match { + case f: Feature[P] => fc.encodeValue(f, out) + case fc: FeatureCollection[P] => fcc.encodeValue(fc, out) + case _ => geometryCodec.encodeValue(x.asInstanceOf[Geometry], out) + } + + override def nullValue: GeoJson[P] = null.asInstanceOf[GeoJson[P]] + } +} diff --git a/jsoniter-scala/src/test/scala/com/free2move/geoscala/JsoniterScalaDecodingTests.scala b/jsoniter-scala/src/test/scala/com/free2move/geoscala/JsoniterScalaDecodingTests.scala new file mode 100644 index 0000000..f66b682 --- /dev/null +++ b/jsoniter-scala/src/test/scala/com/free2move/geoscala/JsoniterScalaDecodingTests.scala @@ -0,0 +1,89 @@ +/* + * Copyright 2019 GHM Mobile Development GmbH + * + * 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 com.free2move.geoscala + +import com.github.plokhotnyuk.jsoniter_scala.core._ +import com.github.plokhotnyuk.jsoniter_scala.macros._ +import org.scalatest.EitherValues +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class JsoniterScalaDecodingTests extends AnyFlatSpec with Matchers with EitherValues { + + import com.free2move.geoscala.jsoniter_scala._ + + "The jsoniter-scala codecs" should "handle simple 2D points" in { + val json = + """{ + "type": "Point", + "coordinates": [ + 12.3046875, + 51.8357775 + ] + }""" + readFromString[Point](json) shouldBe Point(Coordinate(12.3046875, 51.8357775)) + readFromString[Geometry](json) shouldBe Point(Coordinate(12.3046875, 51.8357775)) + } + + it should "handle points with more dimensions" in { + val json = + """{ + "type": "Point", + "coordinates": [ + 12.3046875, + 51.8357775, + 7.000, + 42.12345 + ] + }""" + readFromString[Point](json) shouldBe Point(Coordinate(12.3046875, 51.8357775)) + readFromString[Geometry](json) shouldBe Point(Coordinate(12.3046875, 51.8357775)) + } + + it should "handle FeatureCollection without Properties as pure JSON correctly" in { + type Json = Map[String, Int] + + implicit val jsonCodec: JsonValueCodec[Json] = JsonCodecMaker.make + + val json = + """{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "id": 7 + }, + "geometry": { + "type": "Point", + "coordinates": [ + 12.3046875, + 51.8357775 + ] + } + } + ] + }""" + readFromString[FeatureCollection[Json]](json) shouldBe FeatureCollection( + List(Feature(Map("id" -> 7), Point(Coordinate(12.3046875, 51.8357775)))) + ) + readFromString[GeoJson[Json]](json) shouldBe FeatureCollection( + List(Feature(Map("id" -> 7), Point(Coordinate(12.3046875, 51.8357775)))) + ) + } + +} diff --git a/project/plugins.sbt b/project/plugins.sbt index cdb11b1..095ac22 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,5 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.13") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.1") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.5") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.11.0") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.7") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.2.0")