From 8e704c87e49e36d95eed7e114e3ef639bf28c8dd Mon Sep 17 00:00:00 2001 From: Jens Date: Wed, 16 Nov 2022 11:58:36 +0100 Subject: [PATCH 1/3] Enable scala 3 for uPickle Get rid of private lazy val as this doesn't compile for Scala 3. ``` non-private method unmarshaller in trait UpickleCustomizationSupport refers to private lazy value apiInstance ``` by making the def api, a val, we work around this limitation --- .../UpickleCustomizationSupport.scala | 24 +++++++++---------- .../akkahttpupickle/UpickleSupport.scala | 2 +- .../akkahttpupickle/ExampleApp.scala | 4 ++-- .../UpickleCustomizationSupportSpec.scala | 4 ++-- .../akkahttpupickle/UpickleSupportSpec.scala | 2 +- build.sbt | 6 ++--- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupport.scala b/akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupport.scala index 8a20fdca..9d430d2c 100644 --- a/akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupport.scala +++ b/akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupport.scala @@ -58,9 +58,7 @@ trait UpickleCustomizationSupport { type Api <: upickle.Api - def api: Api - - private lazy val apiInstance: Api = api + val api: Api def unmarshallerContentTypes: Seq[ContentTypeRange] = mediaTypes.map(ContentTypeRange.apply) @@ -85,11 +83,11 @@ trait UpickleCustomizationSupport { } private def jsonSource[A](entitySource: SourceOf[A])(implicit - writes: apiInstance.Writer[A], + writes: api.Writer[A], support: JsonEntityStreamingSupport ): SourceOf[ByteString] = entitySource - .map(apiInstance.write(_)) + .map(api.write(_)) .map(ByteString(_)) .via(support.framingRenderer) @@ -101,8 +99,8 @@ trait UpickleCustomizationSupport { * @return * unmarshaller for any `A` value */ - implicit def fromByteStringUnmarshaller[A: apiInstance.Reader]: Unmarshaller[ByteString, A] = - Unmarshaller(_ => bs => Future.fromTry(Try(apiInstance.read(bs.toArray)))) + implicit def fromByteStringUnmarshaller[A: api.Reader]: Unmarshaller[ByteString, A] = + Unmarshaller(_ => bs => Future.fromTry(Try(api.read(bs.toArray)))) /** * HTTP entity => `A` @@ -112,8 +110,8 @@ trait UpickleCustomizationSupport { * @return * unmarshaller for `A` */ - implicit def unmarshaller[A: apiInstance.Reader]: FromEntityUnmarshaller[A] = - jsonStringUnmarshaller(this).map(apiInstance.read(_)) + implicit def unmarshaller[A: api.Reader]: FromEntityUnmarshaller[A] = + jsonStringUnmarshaller(this).map(api.read(_)) /** * `A` => HTTP entity @@ -123,8 +121,8 @@ trait UpickleCustomizationSupport { * @return * marshaller for any `A` value */ - implicit def marshaller[A: apiInstance.Writer]: ToEntityMarshaller[A] = - jsonStringMarshaller(this).compose(apiInstance.write(_)) + implicit def marshaller[A: api.Writer]: ToEntityMarshaller[A] = + jsonStringMarshaller(this).compose(api.write(_)) /** * HTTP entity => `Source[A, _]` @@ -134,7 +132,7 @@ trait UpickleCustomizationSupport { * @return * unmarshaller for `Source[A, _]` */ - implicit def sourceUnmarshaller[A: apiInstance.Reader](implicit + implicit def sourceUnmarshaller[A: api.Reader](implicit support: JsonEntityStreamingSupport = EntityStreamingSupport.json() ): FromEntityUnmarshaller[SourceOf[A]] = Unmarshaller @@ -165,7 +163,7 @@ trait UpickleCustomizationSupport { * marshaller for any `SourceOf[A]` value */ implicit def sourceMarshaller[A](implicit - writes: apiInstance.Writer[A], + writes: api.Writer[A], support: JsonEntityStreamingSupport = EntityStreamingSupport.json() ): ToEntityMarshaller[SourceOf[A]] = jsonSourceStringMarshaller(this).compose(jsonSource[A]) diff --git a/akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleSupport.scala b/akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleSupport.scala index 7ee40f18..dbd62a9e 100644 --- a/akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleSupport.scala +++ b/akka-http-upickle/src/main/scala/de/heikoseeberger/akkahttpupickle/UpickleSupport.scala @@ -28,5 +28,5 @@ trait UpickleSupport extends UpickleCustomizationSupport { override type Api = upickle.default.type - override def api: Api = upickle.default + override val api: Api = upickle.default } diff --git a/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/ExampleApp.scala b/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/ExampleApp.scala index 154bf5ca..f020fbcd 100644 --- a/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/ExampleApp.scala +++ b/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/ExampleApp.scala @@ -36,7 +36,7 @@ object ExampleApp { final case class Foo(bar: String) def main(args: Array[String]): Unit = { - implicit val system = ActorSystem() + implicit val system: ActorSystem = ActorSystem() Http().newServerAt("127.0.0.1", 8000).bindFlow(route) @@ -58,7 +58,7 @@ object ExampleApp { } } ~ pathPrefix("stream") { post { - entity(as[SourceOf[Foo]]) { fooSource: SourceOf[Foo] => + entity(as[SourceOf[Foo]]) { (fooSource: SourceOf[Foo]) => complete(fooSource.throttle(1, 2.seconds)) } } ~ get { diff --git a/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupportSpec.scala b/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupportSpec.scala index 6c24e1b3..416519c1 100644 --- a/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupportSpec.scala +++ b/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleCustomizationSupportSpec.scala @@ -33,7 +33,7 @@ final class UpickleCustomizationSupportSpec with Matchers with BeforeAndAfterAll { - private implicit val system = ActorSystem() + private implicit val system: ActorSystem = ActorSystem() object FooApi extends AttributeTagged { override implicit val IntWriter: FooApi.Writer[Int] = new Writer[Int] { @@ -42,7 +42,7 @@ final class UpickleCustomizationSupportSpec } object UpickleFoo extends UpickleCustomizationSupport { override type Api = FooApi.type - override def api: FooApi.type = FooApi + override val api: FooApi.type = FooApi } import UpickleFoo._ diff --git a/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleSupportSpec.scala b/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleSupportSpec.scala index 299f6cbd..55ef96e9 100644 --- a/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleSupportSpec.scala +++ b/akka-http-upickle/src/test/scala/de/heikoseeberger/akkahttpupickle/UpickleSupportSpec.scala @@ -46,7 +46,7 @@ final class UpickleSupportSpec extends AsyncWordSpec with Matchers with BeforeAn import UpickleSupport._ import UpickleSupportSpec._ - private implicit val system = ActorSystem() + private implicit val system: ActorSystem = ActorSystem() "UpickleSupport" should { "enable marshalling and unmarshalling of case classes" in { diff --git a/build.sbt b/build.sbt index cf21bdf8..0c885663 100644 --- a/build.sbt +++ b/build.sbt @@ -171,7 +171,7 @@ lazy val `akka-http-play-json` = lazy val `akka-http-upickle` = project .enablePlugins(AutomateHeaderPlugin) - .settings(commonSettings) + .settings(commonSettings, withScala3) .settings( libraryDependencies ++= Seq( library.akkaHttp, @@ -237,8 +237,8 @@ lazy val library = val jsoniterScala = "2.17.9" val ninny = "0.7.0" val play = "2.9.2" - val scalaTest = "3.2.11" - val upickle = "1.5.0" + val scalaTest = "3.2.14" + val upickle = "1.6.0" val zioJson = "0.3.0" } // format: off From 6ea80f7360a5e5aff77bd655c0e02e8f84cce088 Mon Sep 17 00:00:00 2001 From: Jens Date: Sat, 19 Nov 2022 22:30:44 +0100 Subject: [PATCH 2/3] Enable scala 3 for Jackson Do not use typetag for Jackson but classtag --- .../akkahttpjackson/JacksonSupport.scala | 56 ++++++------------- .../akkahttpjackson/ExampleApp.scala | 5 +- .../akkahttpjackson/JacksonSupportSpec.scala | 3 +- build.sbt | 7 +-- 4 files changed, 24 insertions(+), 47 deletions(-) diff --git a/akka-http-jackson/src/main/scala/de/heikoseeberger/akkahttpjackson/JacksonSupport.scala b/akka-http-jackson/src/main/scala/de/heikoseeberger/akkahttpjackson/JacksonSupport.scala index 14f7506f..7bf16765 100644 --- a/akka-http-jackson/src/main/scala/de/heikoseeberger/akkahttpjackson/JacksonSupport.scala +++ b/akka-http-jackson/src/main/scala/de/heikoseeberger/akkahttpjackson/JacksonSupport.scala @@ -20,29 +20,27 @@ import akka.http.javadsl.common.JsonEntityStreamingSupport import akka.http.javadsl.marshallers.jackson.Jackson import akka.http.scaladsl.common.EntityStreamingSupport import akka.http.scaladsl.marshalling._ -import akka.http.scaladsl.model.{ ContentTypeRange, HttpEntity, MediaType, MessageEntity } import akka.http.scaladsl.model.MediaTypes.`application/json` +import akka.http.scaladsl.model.{ ContentTypeRange, HttpEntity, MediaType, MessageEntity } import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal, Unmarshaller } import akka.http.scaladsl.util.FastFuture import akka.stream.scaladsl.{ Flow, Source } import akka.util.ByteString -import com.fasterxml.jackson.core.`type`.TypeReference import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.scala.DefaultScalaModule -import java.lang.reflect.{ ParameterizedType, Type => JType } -import scala.collection.immutable.Seq +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.scala.{ ClassTagExtensions, DefaultScalaModule, JavaTypeable } + import scala.concurrent.Future -import scala.reflect.runtime.universe._ +import scala.reflect.ClassTag import scala.util.Try import scala.util.control.NonFatal /** - * Automatic to and from JSON marshalling/unmarshalling usung an in-scope Jackon's ObjectMapper + * Automatic to and from JSON marshalling/unmarshalling using an in-scope Jackson ObjectMapper */ object JacksonSupport extends JacksonSupport { - - val defaultObjectMapper: ObjectMapper = - new ObjectMapper().registerModule(DefaultScalaModule) + val defaultObjectMapper: ObjectMapper with ClassTagExtensions = + JsonMapper.builder().addModule(DefaultScalaModule).build() :: ClassTagExtensions } /** @@ -67,24 +65,6 @@ trait JacksonSupport { case (data, charset) => data.decodeString(charset.nioCharset.name) } - private def typeReference[T: TypeTag] = { - val t = typeTag[T] - val mirror = t.mirror - def mapType(t: Type): JType = - if (t.typeArgs.isEmpty) - mirror.runtimeClass(t) - else - new ParameterizedType { - def getRawType() = mirror.runtimeClass(t) - def getActualTypeArguments() = t.typeArgs.map(mapType).toArray - def getOwnerType() = null - } - - new TypeReference[T] { - override def getType = mapType(t.tpe) - } - } - private def sourceByteStringMarshaller( mediaType: MediaType.WithFixedCharset ): Marshaller[SourceOf[ByteString], MessageEntity] = @@ -116,11 +96,10 @@ trait JacksonSupport { /** * HTTP entity => `A` */ - implicit def unmarshaller[A](implicit - ct: TypeTag[A], - objectMapper: ObjectMapper = defaultObjectMapper + implicit def unmarshaller[A: JavaTypeable](implicit + objectMapper: ObjectMapper with ClassTagExtensions = defaultObjectMapper ): FromEntityUnmarshaller[A] = - jsonStringUnmarshaller.map(data => objectMapper.readValue(data, typeReference[A])) + jsonStringUnmarshaller.map(data => objectMapper.readValue[A](data)) /** * `A` => HTTP entity @@ -138,12 +117,11 @@ trait JacksonSupport { * @return * unmarshaller for any `A` value */ - implicit def fromByteStringUnmarshaller[A](implicit - ct: TypeTag[A], - objectMapper: ObjectMapper = defaultObjectMapper + implicit def fromByteStringUnmarshaller[A: ClassTag](implicit + objectMapper: ObjectMapper with ClassTagExtensions = defaultObjectMapper ): Unmarshaller[ByteString, A] = Unmarshaller { _ => bs => - Future.fromTry(Try(objectMapper.readValue(bs.toArray, typeReference[A]))) + Future.fromTry(Try(objectMapper.readValue[A](bs.toArray))) } /** @@ -154,9 +132,8 @@ trait JacksonSupport { * @return * unmarshaller for `Source[A, _]` */ - implicit def sourceUnmarshaller[A](implicit - ct: TypeTag[A], - objectMapper: ObjectMapper = defaultObjectMapper, + implicit def sourceUnmarshaller[A: ClassTag](implicit + objectMapper: ObjectMapper with ClassTagExtensions = defaultObjectMapper, support: JsonEntityStreamingSupport = EntityStreamingSupport.json() ): FromEntityUnmarshaller[SourceOf[A]] = Unmarshaller @@ -187,7 +164,6 @@ trait JacksonSupport { * marshaller for any `SourceOf[A]` value */ implicit def sourceMarshaller[A](implicit - ct: TypeTag[A], objectMapper: ObjectMapper = defaultObjectMapper, support: JsonEntityStreamingSupport = EntityStreamingSupport.json() ): ToEntityMarshaller[SourceOf[A]] = diff --git a/akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/ExampleApp.scala b/akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/ExampleApp.scala index 692fae15..f0cb753d 100644 --- a/akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/ExampleApp.scala +++ b/akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/ExampleApp.scala @@ -22,6 +22,7 @@ import akka.http.scaladsl.model.HttpRequest import akka.http.scaladsl.server.Directives import akka.http.scaladsl.unmarshalling.Unmarshal import akka.stream.scaladsl.Source + import scala.concurrent.Await import scala.concurrent.duration._ import scala.io.StdIn @@ -31,7 +32,7 @@ object ExampleApp { final case class Foo(bar: String) def main(args: Array[String]): Unit = { - implicit val system = ActorSystem() + implicit val system: ActorSystem = ActorSystem() // provide an implicit ObjectMapper if you want serialization/deserialization to use it // instead of a default ObjectMapper configured only with DefaultScalaModule provided @@ -64,7 +65,7 @@ object ExampleApp { } } ~ pathPrefix("stream") { post { - entity(as[SourceOf[Foo]]) { fooSource: SourceOf[Foo] => + entity(as[SourceOf[Foo]]) { (fooSource: SourceOf[Foo]) => complete(fooSource.throttle(1, 2.seconds)) } } ~ get { diff --git a/akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/JacksonSupportSpec.scala b/akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/JacksonSupportSpec.scala index 84c3ddc4..5f753790 100644 --- a/akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/JacksonSupportSpec.scala +++ b/akka-http-jackson/src/test/scala/de/heikoseeberger/akkahttpjackson/JacksonSupportSpec.scala @@ -26,6 +26,7 @@ import akka.stream.scaladsl.{ Sink, Source } import org.scalatest.BeforeAndAfterAll import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AsyncWordSpec + import scala.concurrent.Await import scala.concurrent.duration.DurationInt @@ -40,7 +41,7 @@ final class JacksonSupportSpec extends AsyncWordSpec with Matchers with BeforeAn import JacksonSupport._ import JacksonSupportSpec._ - private implicit val system = ActorSystem() + private implicit val system: ActorSystem = ActorSystem() "JacksonSupport" should { "should enable marshalling and unmarshalling of case classes" in { diff --git a/build.sbt b/build.sbt index 0c885663..e84c154b 100644 --- a/build.sbt +++ b/build.sbt @@ -101,15 +101,14 @@ lazy val `akka-http-circe` = lazy val `akka-http-jackson` = project .enablePlugins(AutomateHeaderPlugin) - .settings(commonSettings) + .settings(commonSettings, withScala3) .settings( libraryDependencies ++= Seq( library.akkaHttp, library.akkaHttpJacksonJava, library.jacksonModuleScala, - "org.scala-lang" % "scala-reflect" % scalaVersion.value, library.akkaStream % Provided, - library.scalaTest % Test, + library.scalaTest % Test, ) ) @@ -232,7 +231,7 @@ lazy val library = val argonaut = "6.3.8" val avro4s = "4.0.12" val circe = "0.14.1" - val jacksonModuleScala = "2.13.1" + val jacksonModuleScala = "2.14.0" val json4s = "4.0.6" val jsoniterScala = "2.17.9" val ninny = "0.7.0" From d04ba011c068e2911af0dfb7fd15d8a30e68fb1c Mon Sep 17 00:00:00 2001 From: Jens Date: Sat, 19 Nov 2022 23:15:46 +0100 Subject: [PATCH 3/3] Add scala 3 support for circe Add missing comma --- .../akkahttpcirce/CirceSupportSpec.scala | 84 +++++++++++++++++++ .../akkahttpcirce/CirceSupportSpec.scala | 75 +++++++++++++++++ .../akkahttpcirce/CirceSupportSpec.scala | 35 ++------ .../akkahttpcirce/ExampleApp.scala | 4 +- build.sbt | 26 +++++- 5 files changed, 188 insertions(+), 36 deletions(-) create mode 100644 akka-http-circe/src/test/scala-2/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala create mode 100644 akka-http-circe/src/test/scala-3/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala diff --git a/akka-http-circe/src/test/scala-2/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala b/akka-http-circe/src/test/scala-2/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala new file mode 100644 index 00000000..2a32f0ce --- /dev/null +++ b/akka-http-circe/src/test/scala-2/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2015 Heiko Seeberger + * + * 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 de.heikoseeberger.akkahttpcirce + +import akka.actor.ActorSystem +import akka.http.scaladsl.marshalling.Marshal +import akka.http.scaladsl.model._ +import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.stream.scaladsl.{ Sink, Source } +import io.circe.Encoder +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AsyncWordSpec +import org.scalatest.{ BeforeAndAfterAll, EitherValues } + +import scala.concurrent.Await +import scala.concurrent.duration.DurationInt + +final class CirceSupportSpec2 + extends AsyncWordSpec + with Matchers + with BeforeAndAfterAll + with ScalaFutures + with EitherValues { + + import CirceSupportSpec._ + + private implicit val system: ActorSystem = ActorSystem() + + /** + * Specs common to both [[FailFastCirceSupport]] and [[ErrorAccumulatingCirceSupport]] + */ + private def commonCirceSupport(support: BaseCirceSupport) = { + import io.circe.generic.auto._ + import support._ + + "enable streamed marshalling and unmarshalling for json arrays" in { + val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList + + // Don't know why, the encoder is not resolving alongside the marshaller + // this only happens if we use the implicits from BaseCirceSupport + // so, tried to create it before and guess what? it worked. + // not sure if this is a bug, but, the error is this: + // diverging implicit expansion for type io.circe.Encoder[A] + // [error] starting with lazy value encodeZoneOffset in object Encoder + implicit val e = implicitly[Encoder[Foo]] + + Marshal(Source(foos)) + .to[ResponseEntity] + .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) + .flatMap(_.runWith(Sink.seq)) + .map(_ shouldBe foos) + } + + } + + "FailFastCirceSupport" should { + behave like commonCirceSupport(FailFastCirceSupport) + } + + "ErrorAccumulatingCirceSupport" should { + behave like commonCirceSupport(ErrorAccumulatingCirceSupport) + + } + + override protected def afterAll(): Unit = { + Await.ready(system.terminate(), 42.seconds) + super.afterAll() + } +} diff --git a/akka-http-circe/src/test/scala-3/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala b/akka-http-circe/src/test/scala-3/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala new file mode 100644 index 00000000..54eef8e6 --- /dev/null +++ b/akka-http-circe/src/test/scala-3/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Heiko Seeberger + * + * 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 de.heikoseeberger.akkahttpcirce + +import akka.actor.ActorSystem +import akka.http.scaladsl.marshalling.Marshal +import akka.http.scaladsl.model._ +import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.stream.scaladsl.{ Sink, Source } +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AsyncWordSpec +import org.scalatest.{ BeforeAndAfterAll, EitherValues } + +import scala.concurrent.Await +import scala.concurrent.duration.DurationInt + +final class CirceSupportSpec2 + extends AsyncWordSpec + with Matchers + with BeforeAndAfterAll + with ScalaFutures + with EitherValues { + + import CirceSupportSpec._ + + private implicit val system: ActorSystem = ActorSystem() + + /** + * Specs common to both [[FailFastCirceSupport]] and [[ErrorAccumulatingCirceSupport]] + */ + private def commonCirceSupport(support: BaseCirceSupport) = { + import io.circe.generic.auto._ + import support._ + + "enable streamed marshalling and unmarshalling for json arrays" in { + val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList + + Marshal(Source(foos)) + .to[ResponseEntity] + .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) + .flatMap(_.runWith(Sink.seq)) + .map(_ shouldBe foos) + } + + } + + "FailFastCirceSupport" should { + behave like commonCirceSupport(FailFastCirceSupport) + } + + "ErrorAccumulatingCirceSupport" should { + behave like commonCirceSupport(ErrorAccumulatingCirceSupport) + + } + + override protected def afterAll(): Unit = { + Await.ready(system.terminate(), 42.seconds) + super.afterAll() + } +} diff --git a/akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala b/akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala index 2171cd54..a8833e19 100644 --- a/akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala +++ b/akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/CirceSupportSpec.scala @@ -18,25 +18,18 @@ package de.heikoseeberger.akkahttpcirce import akka.actor.ActorSystem import akka.http.scaladsl.marshalling.Marshal -import akka.http.scaladsl.model.{ - ContentTypeRange, - HttpCharsets, - HttpEntity, - MediaType, - RequestEntity, - ResponseEntity -} import akka.http.scaladsl.model.ContentTypes.{ `application/json`, `text/plain(UTF-8)` } -import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } +import akka.http.scaladsl.model._ import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException -import akka.stream.scaladsl.{ Sink, Source } +import akka.http.scaladsl.unmarshalling.{ Unmarshal, Unmarshaller } import cats.data.{ NonEmptyList, ValidatedNel } -import io.circe.{ DecodingFailure, Encoder, ParsingFailure, Printer } import io.circe.CursorOp.DownField -import org.scalatest.{ BeforeAndAfterAll, EitherValues } +import io.circe.{ DecodingFailure, ParsingFailure, Printer } import org.scalatest.concurrent.ScalaFutures import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AsyncWordSpec +import org.scalatest.{ BeforeAndAfterAll, EitherValues } + import scala.concurrent.Await import scala.concurrent.duration.DurationInt @@ -79,24 +72,6 @@ final class CirceSupportSpec .map(_ shouldBe foo) } - "enable streamed marshalling and unmarshalling for json arrays" in { - val foos = (0 to 100).map(i => Foo(s"bar-$i")).toList - - // Don't know why, the encoder is not resolving alongside the marshaller - // this only happens if we use the implicits from BaseCirceSupport - // so, tried to create it before and guess what? it worked. - // not sure if this is a bug, but, the error is this: - // diverging implicit expansion for type io.circe.Encoder[A] - // [error] starting with lazy value encodeZoneOffset in object Encoder - implicit val e = implicitly[Encoder[Foo]] - - Marshal(Source(foos)) - .to[ResponseEntity] - .flatMap(entity => Unmarshal(entity).to[SourceOf[Foo]]) - .flatMap(_.runWith(Sink.seq)) - .map(_ shouldBe foos) - } - "provide proper error messages for requirement errors" in { val entity = HttpEntity(`application/json`, """{ "bar": "baz" }""") Unmarshal(entity) diff --git a/akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/ExampleApp.scala b/akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/ExampleApp.scala index 2045d8d0..1f407706 100644 --- a/akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/ExampleApp.scala +++ b/akka-http-circe/src/test/scala/de/heikoseeberger/akkahttpcirce/ExampleApp.scala @@ -31,7 +31,7 @@ object ExampleApp { private final case class Foo(bar: String) def main(args: Array[String]): Unit = { - implicit val system = ActorSystem() + implicit val system: ActorSystem = ActorSystem() Http().newServerAt("127.0.0.1", 8000).bindFlow(route) @@ -54,7 +54,7 @@ object ExampleApp { } } ~ pathPrefix("stream") { post { - entity(as[SourceOf[Foo]]) { fooSource: SourceOf[Foo] => + entity(as[SourceOf[Foo]]) { (fooSource: SourceOf[Foo]) => import sys._ Marshal(Source.single(Foo("a"))).to[RequestEntity] diff --git a/build.sbt b/build.sbt index e84c154b..1468eb23 100644 --- a/build.sbt +++ b/build.sbt @@ -2,6 +2,7 @@ // Build settings // ***************************************************************************** +lazy val scalaReleaseVersion = SettingKey[Int]("scalaReleaseVersion") inThisBuild( Seq( organization := "de.heikoseeberger", @@ -35,7 +36,13 @@ inThisBuild( "-target:jvm-1.8" ), scalafmtOnCompile := true, - dynverSeparator := "_" // the default `+` is not compatible with docker tags, + dynverSeparator := "_", // the default `+` is not compatible with docker tags, + scalaReleaseVersion := { + lazy val v = scalaVersion.value + CrossVersion.partialVersion(v).map(_._1.toInt).getOrElse { + throw new RuntimeException(s"could not get Scala release version from $v") + } + } ) ) @@ -86,7 +93,7 @@ lazy val `akka-http-argonaut` = lazy val `akka-http-circe` = project .enablePlugins(AutomateHeaderPlugin) - .settings(commonSettings) + .settings(commonSettings, withScala3) .settings( libraryDependencies ++= Seq( library.akkaHttp, @@ -94,8 +101,19 @@ lazy val `akka-http-circe` = library.circeParser, library.akkaStream % Provided, library.circeGeneric % Test, - library.scalaTest % Test, - ) + library.scalaTest % Test, + ), + Test / unmanagedSourceDirectories ++= { + if (scalaReleaseVersion.value > 2) { + Seq( + (LocalRootProject / baseDirectory).value / "src" / "test" / "scala-3" + ) + } else { + Seq( + (LocalRootProject / baseDirectory).value / "src" / "test" / "scala-2", + ) + } + } ) lazy val `akka-http-jackson` =