From c03d2a6b8a4e3d295b0b2ab7a5628da290291a1c Mon Sep 17 00:00:00 2001 From: Milad Khajavi Date: Tue, 14 Jan 2025 17:03:35 +0000 Subject: [PATCH 01/13] Fix Issues with Line Numbering in Documentation Examples (#3265) --- docs/examples/websocket.md | 8 ++-- docs/reference/client.md | 2 +- .../websocket/WebSocketClientAdvanced.scala | 48 +++++++++++++++++++ .../{ => websocket}/WebSocketEcho.scala | 2 +- .../WebSocketReconnectingClient.scala | 2 +- .../WebSocketServerAdvanced.scala} | 45 +---------------- .../WebSocketSimpleClient.scala | 2 +- 7 files changed, 58 insertions(+), 51 deletions(-) create mode 100644 zio-http-example/src/main/scala/example/websocket/WebSocketClientAdvanced.scala rename zio-http-example/src/main/scala/example/{ => websocket}/WebSocketEcho.scala (97%) rename zio-http-example/src/main/scala/example/{ => websocket}/WebSocketReconnectingClient.scala (98%) rename zio-http-example/src/main/scala/example/{WebSocketAdvanced.scala => websocket/WebSocketServerAdvanced.scala} (61%) rename zio-http-example/src/main/scala/example/{ => websocket}/WebSocketSimpleClient.scala (97%) diff --git a/docs/examples/websocket.md b/docs/examples/websocket.md index 52d5683617..a49a85ef66 100644 --- a/docs/examples/websocket.md +++ b/docs/examples/websocket.md @@ -21,12 +21,12 @@ Our WebSocketApp will handle the following events send by the client: * If the client sends "end", we will close the connection. * If the client sends any other message, we will send the same message back to the client 10 times. -For the client to establish a connection with the server, we offer the `/subscriptions` endpoint. +For the client to establish a connection with the server, we offer the `/subscriptions` endpoint: ```scala mdoc:passthrough import utils._ -printSource("zio-http-example/src/main/scala/example/WebSocketAdvanced.scala", lines=Seq((3, 7), (9, 60)), showLineNumbers=false) +printSource("zio-http-example/src/main/scala/example/websocket/WebSocketServerAdvanced.scala") ``` A few things worth noting: @@ -47,7 +47,7 @@ All we need for that, is the URL of the server. In our case it's `"ws://localhos ```scala mdoc:passthrough import utils._ -printSource("zio-http-example/src/main/scala/example/WebSocketAdvanced.scala", lines=Seq((3, 7), (62, 99)), showLineNumbers=false) +printSource("zio-http-example/src/main/scala/example/websocket/WebSocketClientAdvanced.scala") ``` While we access here `Queue[String]` via the ZIO environment, you should use a service in a real world application, that requires a queue as one of its constructor dependencies. @@ -59,5 +59,5 @@ See [ZIO Services](https://zio.dev/reference/service-pattern/) for more informat ```scala mdoc:passthrough import utils._ -printSource("zio-http-example/src/main/scala/example/WebSocketEcho.scala") +printSource("zio-http-example/src/main/scala/example/websocket/WebSocketEcho.scala") ``` diff --git a/docs/reference/client.md b/docs/reference/client.md index e334de9225..fad0a1a28c 100644 --- a/docs/reference/client.md +++ b/docs/reference/client.md @@ -511,5 +511,5 @@ This example represents a WebSocket client application that automatically attemp ```scala mdoc:passthrough import utils._ -printSource("zio-http-example/src/main/scala/example/WebSocketReconnectingClient.scala") +printSource("zio-http-example/src/main/scala/example/websocket/WebSocketReconnectingClient.scala") ``` diff --git a/zio-http-example/src/main/scala/example/websocket/WebSocketClientAdvanced.scala b/zio-http-example/src/main/scala/example/websocket/WebSocketClientAdvanced.scala new file mode 100644 index 0000000000..8247b01951 --- /dev/null +++ b/zio-http-example/src/main/scala/example/websocket/WebSocketClientAdvanced.scala @@ -0,0 +1,48 @@ +package example.websocket +import scala.annotation.nowarn + +import zio._ + +import zio.http.ChannelEvent.Read +import zio.http._ + +object WebSocketSimpleClientAdvanced extends ZIOAppDefault { + + def sendChatMessage(message: String): ZIO[Queue[String], Throwable, Unit] = + ZIO.serviceWithZIO[Queue[String]](_.offer(message).unit) + + def processQueue(channel: WebSocketChannel): ZIO[Queue[String], Throwable, Unit] = { + for { + queue <- ZIO.service[Queue[String]] + msg <- queue.take + _ <- channel.send(Read(WebSocketFrame.Text(msg))) + } yield () + }.forever.forkDaemon.unit + + private def webSocketHandler: ZIO[Queue[String] with Client with Scope, Throwable, Response] = + Handler.webSocket { channel => + for { + _ <- processQueue(channel) + _ <- channel.receiveAll { + case Read(WebSocketFrame.Text(text)) => + Console.printLine(s"Server: $text") + case _ => + ZIO.unit + } + } yield () + }.connect("ws://localhost:8080/subscriptions") + + @nowarn("msg=dead code") + override val run = + ZIO + .scoped(for { + _ <- webSocketHandler + _ <- Console.readLine.flatMap(sendChatMessage).forever.forkDaemon + _ <- ZIO.never + } yield ()) + .provide( + Client.default, + ZLayer(Queue.bounded[String](100)), + ) + +} diff --git a/zio-http-example/src/main/scala/example/WebSocketEcho.scala b/zio-http-example/src/main/scala/example/websocket/WebSocketEcho.scala similarity index 97% rename from zio-http-example/src/main/scala/example/WebSocketEcho.scala rename to zio-http-example/src/main/scala/example/websocket/WebSocketEcho.scala index 245c722f3b..0ba5dc00fa 100644 --- a/zio-http-example/src/main/scala/example/WebSocketEcho.scala +++ b/zio-http-example/src/main/scala/example/websocket/WebSocketEcho.scala @@ -1,4 +1,4 @@ -package example +package example.websocket import zio._ diff --git a/zio-http-example/src/main/scala/example/WebSocketReconnectingClient.scala b/zio-http-example/src/main/scala/example/websocket/WebSocketReconnectingClient.scala similarity index 98% rename from zio-http-example/src/main/scala/example/WebSocketReconnectingClient.scala rename to zio-http-example/src/main/scala/example/websocket/WebSocketReconnectingClient.scala index 2892675243..599ac554ec 100644 --- a/zio-http-example/src/main/scala/example/WebSocketReconnectingClient.scala +++ b/zio-http-example/src/main/scala/example/websocket/WebSocketReconnectingClient.scala @@ -1,4 +1,4 @@ -package example +package example.websocket import zio._ diff --git a/zio-http-example/src/main/scala/example/WebSocketAdvanced.scala b/zio-http-example/src/main/scala/example/websocket/WebSocketServerAdvanced.scala similarity index 61% rename from zio-http-example/src/main/scala/example/WebSocketAdvanced.scala rename to zio-http-example/src/main/scala/example/websocket/WebSocketServerAdvanced.scala index 72b90aab73..19a98f3437 100644 --- a/zio-http-example/src/main/scala/example/WebSocketAdvanced.scala +++ b/zio-http-example/src/main/scala/example/websocket/WebSocketServerAdvanced.scala @@ -1,4 +1,4 @@ -package example +package example.websocket import scala.annotation.nowarn @@ -8,7 +8,7 @@ import zio.http.ChannelEvent.{ExceptionCaught, Read, UserEvent, UserEventTrigger import zio.http._ import zio.http.codec.PathCodec.string -object WebSocketAdvanced extends ZIOAppDefault { +object WebSocketServerAdvanced extends ZIOAppDefault { val socketApp: WebSocketApp[Any] = Handler.webSocket { channel => @@ -60,44 +60,3 @@ object WebSocketAdvanced extends ZIOAppDefault { override val run = Server.serve(routes).provide(Server.default) } - -object WebSocketAdvancedClient extends ZIOAppDefault { - - def sendChatMessage(message: String): ZIO[Queue[String], Throwable, Unit] = - ZIO.serviceWithZIO[Queue[String]](_.offer(message).unit) - - def processQueue(channel: WebSocketChannel): ZIO[Queue[String], Throwable, Unit] = { - for { - queue <- ZIO.service[Queue[String]] - msg <- queue.take - _ <- channel.send(Read(WebSocketFrame.Text(msg))) - } yield () - }.forever.forkDaemon.unit - - private def webSocketHandler: ZIO[Queue[String] with Client with Scope, Throwable, Response] = - Handler.webSocket { channel => - for { - _ <- processQueue(channel) - _ <- channel.receiveAll { - case Read(WebSocketFrame.Text(text)) => - Console.printLine(s"Server: $text") - case _ => - ZIO.unit - } - } yield () - }.connect("ws://localhost:8080/subscriptions") - - @nowarn("msg=dead code") - override val run = - ZIO - .scoped(for { - _ <- webSocketHandler - _ <- Console.readLine.flatMap(sendChatMessage).forever.forkDaemon - _ <- ZIO.never - } yield ()) - .provide( - Client.default, - ZLayer(Queue.bounded[String](100)), - ) - -} diff --git a/zio-http-example/src/main/scala/example/WebSocketSimpleClient.scala b/zio-http-example/src/main/scala/example/websocket/WebSocketSimpleClient.scala similarity index 97% rename from zio-http-example/src/main/scala/example/WebSocketSimpleClient.scala rename to zio-http-example/src/main/scala/example/websocket/WebSocketSimpleClient.scala index 9e7dd8f17a..798935e6c1 100644 --- a/zio-http-example/src/main/scala/example/WebSocketSimpleClient.scala +++ b/zio-http-example/src/main/scala/example/websocket/WebSocketSimpleClient.scala @@ -1,4 +1,4 @@ -package example +package example.websocket import zio._ From 03c055003510e9217fd356970d9eabaa38f6a261 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:20:47 +0100 Subject: [PATCH 02/13] Update CODEOWNERS --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 021e75bced..dea0426407 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @jdegoes @vigoo @adamgfraser @987Nabil +* @jdegoes @vigoo @kyri-petrou @987Nabil From 79ad89dca6beee1cf335db0efc987b06ce81165e Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:21:06 +0100 Subject: [PATCH 03/13] Use empty body for `Unit` payload in Endpoint (#3258) (#3271) --- .../zio/http/endpoint/RoundtripSpec.scala | 5 +++ .../zio/http/codec/internal/BodyCodec.scala | 34 +++++++++++++------ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/zio-http/jvm/src/test/scala/zio/http/endpoint/RoundtripSpec.scala b/zio-http/jvm/src/test/scala/zio/http/endpoint/RoundtripSpec.scala index 3b6fa237f6..d02e01565e 100644 --- a/zio-http/jvm/src/test/scala/zio/http/endpoint/RoundtripSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/endpoint/RoundtripSpec.scala @@ -176,6 +176,11 @@ object RoundtripSpec extends ZIOHttpSpec { Post(20, "title", "body", 10), ) }, + test("simple get without payload") { + val healthCheckAPI = Endpoint(GET / "health-check").out[Unit] + val healthCheckHandler = healthCheckAPI.implementAs(()) + testEndpoint(healthCheckAPI, Routes(healthCheckHandler), (), ()) + }, test("simple get with query params from case class") { val endpoint = Endpoint(GET / "query") .query(HttpCodec.queryAll[Params]) diff --git a/zio-http/shared/src/main/scala/zio/http/codec/internal/BodyCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/internal/BodyCodec.scala index feb9f64ee1..2de9d6ee31 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/internal/BodyCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/internal/BodyCodec.scala @@ -117,9 +117,10 @@ private[http] object BodyCodec { .lookup(field.contentType) .toRight(HttpCodecError.CustomError("UnsupportedMediaType", s"MediaType: ${field.contentType}")) codec0 match { - case Left(error) => ZIO.fail(error) + case Left(error) => + Exit.fail(error) case Right((_, BinaryCodecWithSchema(_, schema))) if schema == Schema[Unit] => - ZIO.unit.asInstanceOf[IO[Throwable, A]] + Exit.unit.asInstanceOf[IO[Throwable, A]] case Right((_, bc @ BinaryCodecWithSchema(_, schema))) => field.asChunk.flatMap { chunk => ZIO.fromEither(bc.codec(config).decode(chunk)) }.flatMap(validateZIO(schema)) } @@ -128,9 +129,11 @@ private[http] object BodyCodec { override def decodeFromBody(body: Body, config: CodecConfig)(implicit trace: Trace): IO[Throwable, A] = { val codec0 = codecForBody(codec, body) codec0 match { - case Left(error) => ZIO.fail(error) + case Left(error) => + Exit.fail(error) case Right((_, BinaryCodecWithSchema(_, schema))) if schema == Schema[Unit] => - ZIO.unit.asInstanceOf[IO[Throwable, A]] + if (body.isEmpty) Exit.unit.asInstanceOf[IO[Throwable, A]] + else ZIO.fail(HttpCodecError.CustomError("InvalidBody", "Non-empty body cannot be decoded as Unit")) case Right((_, bc @ BinaryCodecWithSchema(_, schema))) => body.asChunk.flatMap { chunk => ZIO.fromEither(bc.codec(config).decode(chunk)) }.flatMap(validateZIO(schema)) } @@ -139,7 +142,9 @@ private[http] object BodyCodec { def encodeToField(value: A, mediaTypes: Chunk[MediaTypeWithQFactor], name: String, config: CodecConfig)(implicit trace: Trace, ): FormField = { - val (mediaType, bc @ BinaryCodecWithSchema(_, _)) = codec.chooseFirstOrDefault(mediaTypes) + val selected = codec.chooseFirstOrDefault(mediaTypes) + val mediaType = selected._1 + val bc = selected._2 if (mediaType.binary) { FormField.binaryField( name, @@ -158,8 +163,11 @@ private[http] object BodyCodec { def encodeToBody(value: A, mediaTypes: Chunk[MediaTypeWithQFactor], config: CodecConfig)(implicit trace: Trace, ): Body = { - val (mediaType, bc @ BinaryCodecWithSchema(_, _)) = codec.chooseFirstOrDefault(mediaTypes) - Body.fromChunk(bc.codec(config).encode(value), mediaType) + val selected = codec.chooseFirstOrDefault(mediaTypes) + val mediaType = selected._1 + val bc = selected._2 + if (bc.schema == Schema[Unit]) Body.empty.contentType(mediaType) + else Body.fromChunk(bc.codec(config).encode(value), mediaType) } type Element = A @@ -200,7 +208,9 @@ private[http] object BodyCodec { )(implicit trace: Trace, ): FormField = { - val (mediaType, bc) = codec.chooseFirstOrDefault(mediaTypes) + val selected = codec.chooseFirstOrDefault(mediaTypes) + val mediaType = selected._1 + val bc = selected._2 FormField.streamingBinaryField( name, value >>> bc.codec(config).streamEncoder, @@ -215,7 +225,9 @@ private[http] object BodyCodec { )(implicit trace: Trace, ): Body = { - val (mediaType, bc @ BinaryCodecWithSchema(_, _)) = codec.chooseFirstOrDefault(mediaTypes) + val selected = codec.chooseFirstOrDefault(mediaTypes) + val mediaType = selected._1 + val bc = selected._2 Body.fromStreamChunked(value >>> bc.codec(config).streamEncoder).contentType(mediaType) } @@ -232,8 +244,8 @@ private[http] object BodyCodec { private[internal] def validateZIO[A](schema: Schema[A])(e: A)(implicit trace: Trace): ZIO[Any, HttpCodecError, A] = { val errors = Schema.validate(e)(schema) - if (errors.isEmpty) ZIO.succeed(e) - else ZIO.fail(HttpCodecError.InvalidEntity.wrap(errors)) + if (errors.isEmpty) Exit.succeed(e) + else Exit.fail(HttpCodecError.InvalidEntity.wrap(errors)) } private[internal] def validateStream[E](schema: Schema[E])(implicit From a045bc90c4d2598665902b32ad0e772c370bb4d4 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Sat, 18 Jan 2025 11:47:57 +0100 Subject: [PATCH 04/13] Fix publish CI (#3275) --- .github/workflows/ci.yml | 4 ++++ build.sbt | 1 + 2 files changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfe920e147..ac71ddbc97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -202,6 +202,10 @@ jobs: tar xf targets.tar rm targets.tar + - uses: coursier/setup-action@v1 + with: + apps: sbt + - name: Release env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} diff --git a/build.sbt b/build.sbt index 79099fff0e..28eefe6366 100644 --- a/build.sbt +++ b/build.sbt @@ -56,6 +56,7 @@ ThisBuild / githubWorkflowAddedJobs := ThisBuild / githubWorkflowTargetTags ++= Seq("v*") ThisBuild / githubWorkflowPublishTargetBranches += RefPredicate.StartsWith(Ref.Tag("v")) +ThisBuild / githubWorkflowPublishPreamble := Seq(coursierSetup) ThisBuild / githubWorkflowPublish := Seq( WorkflowStep.Sbt( From 0c1143423edbfaf6547635fc5cd993534da683e2 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:40:31 +0100 Subject: [PATCH 05/13] Option to generate examples for OpenAPI (#3196) (#3273) --- .../endpoint/openapi/OpenAPIGenSpec.scala | 93 +++++++++++++ .../http/endpoint/openapi/OpenAPIGen.scala | 131 +++++++++++++++++- 2 files changed, 218 insertions(+), 6 deletions(-) diff --git a/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala b/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala index 73ceede3a0..6ff5b44fad 100644 --- a/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala @@ -2398,6 +2398,99 @@ object OpenAPIGenSpec extends ZIOSpecDefault { |}""".stripMargin assertTrue(json == toJsonAst(expected)) }, + test("generated example") { + val endpoint = Endpoint(GET / "static").in[NestedProduct] + val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", genExamples = true, endpoint) + val json = toJsonAst(generated) + val expected = """{ + | "openapi" : "3.1.0", + | "info" : { + | "title" : "Simple Endpoint", + | "version" : "1.0" + | }, + | "paths" : { + | "/static" : { + | "get" : { + | "requestBody" : { + | "content" : { + | "application/json" : { + | "schema" : { + | "$ref" : "#/components/schemas/NestedProduct" + | }, + | "examples" : { + | "generated" : { + | "value" : { + | "imageMetadata" : { + | "name" : "", + | "size" : 0 + | }, + | "withOptionalField" : { + | "name" : "", + | "age" : 0 + | } + | } + | } + | } + | } + | }, + | "required" : true + | } + | } + | } + | }, + | "components" : { + | "schemas" : { + | "ImageMetadata" : { + | "type" : "object", + | "properties" : { + | "name" : { + | "type" : "string" + | }, + | "size" : { + | "type" : "integer", + | "format" : "int32" + | } + | }, + | "required" : [ + | "name", + | "size" + | ] + | }, + | "NestedProduct" : { + | "type" : "object", + | "properties" : { + | "imageMetadata" : { + | "$ref" : "#/components/schemas/ImageMetadata" + | }, + | "withOptionalField" : { + | "$ref" : "#/components/schemas/WithOptionalField" + | } + | }, + | "required" : [ + | "imageMetadata", + | "withOptionalField" + | ] + | }, + | "WithOptionalField" : { + | "type" : "object", + | "properties" : { + | "name" : { + | "type" : "string" + | }, + | "age" : { + | "type" : "integer", + | "format" : "int32" + | } + | }, + | "required" : [ + | "name" + | ] + | } + | } + | } + |}""".stripMargin + assertTrue(json == toJsonAst(expected)) + }, test("enum") { val endpoint = Endpoint(GET / "static").in[SimpleEnum] val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", endpoint) diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala index 64ba057807..59ab71fae7 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala @@ -68,7 +68,10 @@ object OpenAPIGen { } def examples(schema: Schema[_]): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] = - examples.map { case (k, v) => + examples(schema, generate = false) + + def examples(schema: Schema[_], generate: Boolean): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] = + (if (generate && examples.isEmpty) generateExamples(schema) else examples).map { case (k, v) => k -> OpenAPI.ReferenceOr.Or(OpenAPI.Example(toJsonAst(schema, v))) } @@ -133,12 +136,15 @@ object OpenAPIGen { ) def contentExamples: Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] = + contentExamples(genExamples = false) + + def contentExamples(genExamples: Boolean): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] = content.flatMap { case mc @ MetaCodec(HttpCodec.Content(codec, _, _), _) if codec.lookup(MediaType.application.json).isDefined => - mc.examples(codec.lookup(MediaType.application.json).get._2.schema) + mc.examples(codec.lookup(MediaType.application.json).get._2.schema, genExamples) case mc @ MetaCodec(HttpCodec.ContentStream(codec, _, _), _) if codec.lookup(MediaType.application.json).isDefined => - mc.examples(codec.lookup(MediaType.application.json).get._2.schema) + mc.examples(codec.lookup(MediaType.application.json).get._2.schema, genExamples) case _ => Map.empty[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] }.toMap @@ -224,6 +230,57 @@ object OpenAPIGen { } } + private def generateExamples(schema: Schema[_], name: String = "generated"): Map[String, Any] = + schema match { + case collection: Schema.Collection[_, _] => + collection match { + case Schema.Sequence(elementSchema, fromChunk, _, _, _) => + elementSchema.defaultValue.map(v => Map(name -> fromChunk(Chunk.single(v)))).getOrElse(Map.empty) + case map @ Schema.Map(_, _, _) => + val example = for { + k <- map.keySchema.defaultValue + v <- map.valueSchema.defaultValue + } yield map.fromChunk(Chunk.single(k -> v)) + example.map(v => Map(name -> v)).getOrElse(Map.empty) + case map @ Schema.NonEmptyMap(_, _, _) => + val example = for { + k <- map.keySchema.defaultValue + v <- map.valueSchema.defaultValue + } yield map.fromChunk(Chunk.single(k -> v)) + example.map(v => Map(name -> v)).getOrElse(Map.empty) + case Schema.NonEmptySequence(elementSchema, fromChunkOption, _, _, _) => + elementSchema.defaultValue.map(v => Map(name -> fromChunkOption(Chunk.single(v)))).getOrElse(Map.empty) + case set @ Schema.Set(elementSchema, _) => + elementSchema.defaultValue + .map(v => set.fromChunk(Chunk.single(v))) + .map(v => Map(name -> v)) + .getOrElse(Map.empty) + + } + case Transform(schema, _, _, _, _) => + generateExamples(schema, name) + case Schema.Optional(schema, _) => + generateExamples(schema, name).map { case (k, v) => k -> Some(v) } + case Schema.Fail(_, _) => + Map.empty + case Schema.Either(left, right, _) => + generateExamples(left, "generatedLeft") ++ generateExamples(right, "generatedRight") + case Schema.Fallback(left, right, _, _) => + generateExamples(left, "generatedLeft") ++ generateExamples(right, "generatedRight") + case Schema.Lazy(schema0) => + generateExamples(schema0()) + case Schema.Dynamic(_) => + Map.empty + case s @ Schema.Primitive(_, _) => + s.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty) + case enumSchema: Schema.Enum[_] => + enumSchema.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty) + case record: Record[_] => + record.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty) + case t: Schema.Tuple2[_, _] => + t.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty) + } + def method(in: Chunk[MetaCodec[SimpleCodec[Method, _]]]): Method = { if (in.size > 1) throw new Exception("Multiple methods not supported") in.collectFirst { case MetaCodec(SimpleCodec.Specified(method: Method), _) => method } @@ -483,6 +540,12 @@ object OpenAPIGen { endpoints: Endpoint[_, _, _, _, _]*, ): OpenAPI = fromEndpoints(endpoint1 +: endpoints) + def fromEndpoints( + genExamples: Boolean, + endpoint1: Endpoint[_, _, _, _, _], + endpoints: Endpoint[_, _, _, _, _]*, + ): OpenAPI = fromEndpoints(genExamples, endpoint1 +: endpoints) + def fromEndpoints( title: String, version: String, @@ -490,6 +553,14 @@ object OpenAPIGen { endpoints: Endpoint[_, _, _, _, _]*, ): OpenAPI = fromEndpoints(title, version, endpoint1 +: endpoints) + def fromEndpoints( + title: String, + version: String, + genExamples: Boolean, + endpoint1: Endpoint[_, _, _, _, _], + endpoints: Endpoint[_, _, _, _, _]*, + ): OpenAPI = fromEndpoints(title, version, genExamples, endpoint1 +: endpoints) + def fromEndpoints( title: String, version: String, @@ -498,21 +569,50 @@ object OpenAPIGen { endpoints: Endpoint[_, _, _, _, _]*, ): OpenAPI = fromEndpoints(title, version, referenceType, endpoint1 +: endpoints) + def fromEndpoints( + title: String, + version: String, + referenceType: SchemaStyle, + genExamples: Boolean, + endpoint1: Endpoint[_, _, _, _, _], + endpoints: Endpoint[_, _, _, _, _]*, + ): OpenAPI = fromEndpoints(title, version, referenceType, genExamples, endpoint1 +: endpoints) + def fromEndpoints( referenceType: SchemaStyle, endpoints: Iterable[Endpoint[_, _, _, _, _]], ): OpenAPI = if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, referenceType)).reduce(_ ++ _) + def fromEndpoints( + referenceType: SchemaStyle, + genExamples: Boolean, + endpoints: Iterable[Endpoint[_, _, _, _, _]], + ): OpenAPI = + if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, referenceType, genExamples)).reduce(_ ++ _) + def fromEndpoints( endpoints: Iterable[Endpoint[_, _, _, _, _]], ): OpenAPI = if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, SchemaStyle.Compact)).reduce(_ ++ _) + def fromEndpoints( + genExamples: Boolean, + endpoints: Iterable[Endpoint[_, _, _, _, _]], + ): OpenAPI = + if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, SchemaStyle.Compact, genExamples)).reduce(_ ++ _) + def fromEndpoints( title: String, version: String, endpoints: Iterable[Endpoint[_, _, _, _, _]], ): OpenAPI = fromEndpoints(endpoints).title(title).version(version) + def fromEndpoints( + title: String, + version: String, + genExamples: Boolean, + endpoints: Iterable[Endpoint[_, _, _, _, _]], + ): OpenAPI = fromEndpoints(genExamples, endpoints).title(title).version(version) + def fromEndpoints( title: String, version: String, @@ -520,9 +620,27 @@ object OpenAPIGen { endpoints: Iterable[Endpoint[_, _, _, _, _]], ): OpenAPI = fromEndpoints(referenceType, endpoints).title(title).version(version) + def fromEndpoints( + title: String, + version: String, + referenceType: SchemaStyle, + genExamples: Boolean, + endpoints: Iterable[Endpoint[_, _, _, _, _]], + ): OpenAPI = fromEndpoints(referenceType, genExamples, endpoints).title(title).version(version) + + def gen( + endpoint: Endpoint[_, _, _, _, _], + ): OpenAPI = gen(endpoint, SchemaStyle.Compact) + + def gen( + endpoint: Endpoint[_, _, _, _, _], + referenceType: SchemaStyle, + ): OpenAPI = gen(endpoint, referenceType, genExamples = false) + def gen( endpoint: Endpoint[_, _, _, _, _], referenceType: SchemaStyle = SchemaStyle.Compact, + genExamples: Boolean, ): OpenAPI = { val inAtoms = AtomizedMetaCodecs.flatten(endpoint.input) val outs: Map[OpenAPI.StatusOrDefault, Map[MediaType, (JsonSchema, AtomizedMetaCodecs)]] = @@ -610,7 +728,7 @@ object OpenAPIGen { val mediaTypeResponses = mediaTypes.map { case (mediaType, (schema, atomized)) => mediaType.fullType -> OpenAPI.MediaType( schema = OpenAPI.ReferenceOr.Or(schema), - examples = atomized.contentExamples, + examples = atomized.contentExamples(genExamples), encoding = Map.empty, ) } @@ -627,7 +745,7 @@ object OpenAPIGen { }) def responses: OpenAPI.Responses = - responsesForAlternatives(outs) + responsesForAlternatives(outs, genExamples) def parameters: Set[OpenAPI.ReferenceOr[OpenAPI.Parameter]] = queryParams ++ pathParams ++ headerParams @@ -992,13 +1110,14 @@ object OpenAPIGen { private def responsesForAlternatives( codecs: Map[OpenAPI.StatusOrDefault, Map[MediaType, (JsonSchema, AtomizedMetaCodecs)]], + genExamples: Boolean, ): Map[OpenAPI.StatusOrDefault, OpenAPI.ReferenceOr[OpenAPI.Response]] = codecs.map { case (status, mediaTypes) => val combinedAtomizedCodecs = mediaTypes.map { case (_, (_, atomized)) => atomized }.reduce(_ ++ _) val mediaTypeResponses = mediaTypes.map { case (mediaType, (schema, atomized)) => mediaType.fullType -> OpenAPI.MediaType( schema = OpenAPI.ReferenceOr.Or(schema), - examples = atomized.contentExamples, + examples = atomized.contentExamples(genExamples), encoding = Map.empty, ) } From 5418a2a3727924df8060981995c53e3d11521844 Mon Sep 17 00:00:00 2001 From: Gilad Hoch Date: Sun, 19 Jan 2025 11:26:11 +0200 Subject: [PATCH 06/13] Revert "[sbt-gen] sbt plugin for openapi to endpoints codegen" (#3278) --- .github/workflows/ci.yml | 8 +- build.sbt | 36 ----- project/BuildHelper.scala | 2 +- .../main/scala/zio/http/gen/sbt/Format.scala | 11 -- .../zio/http/gen/sbt/ZioHttpCodegen.scala | 146 ------------------ .../zio-http-sbt-codegen/dynamic/build.sbt | 44 ------ .../dynamic/project/plugins.sbt | 5 - .../zio-http-sbt-codegen/dynamic/test | 7 - .../zio-http-sbt-codegen/simple/build.sbt | 8 - .../simple/project/plugins.sbt | 5 - .../main/oapi/dev/zoo/service/openapi.yaml | 69 --------- .../sbt-test/zio-http-sbt-codegen/simple/test | 6 - 12 files changed, 2 insertions(+), 345 deletions(-) delete mode 100644 zio-http-gen-sbt-plugin/src/main/scala/zio/http/gen/sbt/Format.scala delete mode 100644 zio-http-gen-sbt-plugin/src/main/scala/zio/http/gen/sbt/ZioHttpCodegen.scala delete mode 100644 zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/build.sbt delete mode 100644 zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/project/plugins.sbt delete mode 100644 zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/test delete mode 100644 zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/build.sbt delete mode 100644 zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/project/plugins.sbt delete mode 100644 zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/src/main/oapi/dev/zoo/service/openapi.yaml delete mode 100644 zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac71ddbc97..10adabc07e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,12 +91,6 @@ jobs: - name: Build project run: sbt '++ ${{ matrix.scala }}' test - - uses: coursier/setup-action@v1 - - - name: Test sbt plugin - if: ${{ github.event_name == 'pull_request' }} && matrix.scala == '2.12.19' - run: sbt ++2.12.19 zioHttpGenSbt/scripted - - uses: coursier/setup-action@v1 with: apps: sbt @@ -112,7 +106,7 @@ jobs: run: sbt '++ ${{ matrix.scala }}' zioHttpShadedTests/test - name: Compress target directories - run: tar cf targets.tar sbt-zio-http-grpc/target zio-http-gen-sbt-plugin/target zio-http-cli/target target zio-http/jvm/target zio-http-docs/target sbt-zio-http-grpc-tests/target zio-http-gen/target zio-http-benchmarks/target zio-http-tools/target zio-http-example/target zio-http-testkit/target zio-http/js/target zio-http-htmx/target project/target + run: tar cf targets.tar sbt-zio-http-grpc/target zio-http-cli/target target zio-http/jvm/target zio-http-docs/target sbt-zio-http-grpc-tests/target zio-http-gen/target zio-http-benchmarks/target zio-http-tools/target zio-http-example/target zio-http-testkit/target zio-http/js/target zio-http-htmx/target project/target - name: Upload target directories uses: actions/upload-artifact@v4 diff --git a/build.sbt b/build.sbt index 28eefe6366..a19027bdb9 100644 --- a/build.sbt +++ b/build.sbt @@ -94,22 +94,6 @@ ThisBuild / githubWorkflowBuildPreamble := Seq( ), ) -ThisBuild / githubWorkflowBuild := { - (ThisBuild / githubWorkflowBuild).value ++ WorkflowJob( - "testSbtPlugin", - "Test sbt plugin", - List( - WorkflowStep.Use(UseRef.Public("coursier", "setup-action", "v1")), - WorkflowStep.Run( - name = Some(s"Test sbt plugin"), - commands = List(s"sbt ++${Scala212} zioHttpGenSbt/scripted"), - cond = Some(s"$${{ github.event_name == 'pull_request' }} && matrix.scala == '$Scala212'"), - ), - ), - scalas = List(Scala212), - ).steps -} - ThisBuild / githubWorkflowBuildPostamble := WorkflowJob( "checkDocGeneration", @@ -367,26 +351,6 @@ lazy val zioHttpGen = (project in file("zio-http-gen")) ) .dependsOn(zioHttpJVM) -lazy val zioHttpGenSbt = (project in file("zio-http-gen-sbt-plugin")) - .enablePlugins(SbtPlugin) - .settings(publishSetting(true)) - .settings( - name := "zio-http-sbt-codegen", - sbtPlugin := true, - scalaVersion := Scala212, - semanticdbEnabled := true, - semanticdbVersion := scalafixSemanticdb.revision, - scalacOptions ++= stdOptions ++ extraOptions(scalaVersion.value), - sbtTestDirectory := sourceDirectory.value / "sbt-test", - scriptedLaunchOpts += ("-Dplugin.version=" + version.value), - scriptedBufferLog := false, - libraryDependencies ++= Seq( - `zio-json-yaml`, - `zio-test`, - `zio-test-sbt`, - ) - ).dependsOn(LocalProject("zioHttpGen")) - lazy val sbtZioHttpGrpc = (project in file("sbt-zio-http-grpc")) .settings(stdSettings("sbt-zio-http-grpc")) .settings(publishSetting(true)) diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 62b15549b1..44e6243a86 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -12,7 +12,7 @@ object BuildHelper extends ScalaSettings { val ScoverageVersion = "2.0.12" val JmhVersion = "0.4.7" - val stdOptions = Seq( + private val stdOptions = Seq( "-deprecation", "-encoding", "UTF-8", diff --git a/zio-http-gen-sbt-plugin/src/main/scala/zio/http/gen/sbt/Format.scala b/zio-http-gen-sbt-plugin/src/main/scala/zio/http/gen/sbt/Format.scala deleted file mode 100644 index 66268483af..0000000000 --- a/zio-http-gen-sbt-plugin/src/main/scala/zio/http/gen/sbt/Format.scala +++ /dev/null @@ -1,11 +0,0 @@ -package zio.http.gen.sbt - -sealed trait Format -object Format { - case object YAML extends Format - case object JSON extends Format - def fromFileName(fileName: String): Format = - if (fileName.endsWith("yml") || fileName.endsWith("yaml")) YAML - else if (fileName.endsWith("json")) JSON - else throw new Exception(s"Unknown format for file $fileName") -} diff --git a/zio-http-gen-sbt-plugin/src/main/scala/zio/http/gen/sbt/ZioHttpCodegen.scala b/zio-http-gen-sbt-plugin/src/main/scala/zio/http/gen/sbt/ZioHttpCodegen.scala deleted file mode 100644 index d2de9ec115..0000000000 --- a/zio-http-gen-sbt-plugin/src/main/scala/zio/http/gen/sbt/ZioHttpCodegen.scala +++ /dev/null @@ -1,146 +0,0 @@ -package zio.http.gen.sbt - -import java.io.File -import java.nio.charset.StandardCharsets -import java.nio.file.StandardOpenOption.{CREATE, TRUNCATE_EXISTING} -import java.nio.file.{Files, Path} - -import scala.io.Source - -import zio.json.yaml._ - -import zio.schema.codec.JsonCodec - -import zio.http.endpoint.openapi.OpenAPI -import zio.http.gen.openapi.{Config, EndpointGen} -import zio.http.gen.scala.CodeGen - -import sbt.Defaults.configSrcSub -import sbt.Keys._ -import sbt._ -import sbt.util.FileInfo - -object ZioHttpCodegen extends AutoPlugin { - - object autoImport { - - val ZIOpenApi = config("oapi") extend Compile - - val zioHttpCodegenMake = taskKey[Seq[File]]("Generate ADTs & endpoints from OpenAPI spec files") - val zioHttpCodegenConf = settingKey[Config]("Configuration for codegen") - val zioHttpCodegenSources = settingKey[File]("Source dir. analoguous to e.g: scalaSource or javaSource") - } - - import autoImport._ - - override lazy val projectSettings: Seq[Setting[_]] = inConfig(ZIOpenApi)( - Seq[Setting[_]]( - zioHttpCodegenSources := (Compile / sourceDirectory).value / "oapi", - sourceGenerators := Nil, - sourceManaged := configSrcSub(sourceManaged).value, - sourceDirectories := List(zioHttpCodegenSources.value, sourceManaged.value), - sources := { - val generatedFiles = Defaults.generate(sourceGenerators).value - streams.value.log.info(s"Generated ${generatedFiles.length} OpenAPI spec files") - sourceDirectories.value.flatMap(listFilesRec) - }, - zioHttpCodegenConf := Config.default, - zioHttpCodegenMake := Def.taskDyn { - - val maybeCached = (ZIOpenApi / zioHttpCodegenMake).previous - val s = streams.value - - val cachedCodegen = Tracked.inputChanged[ - FilesInfo[FileInfo.full.F], - Def.Initialize[Task[Seq[File]]], - ](s.cacheStoreFactory.make("zioapigen")) { (changed, in) => - maybeCached match { - case Some(cached) if !changed => - Def.task { - s.log.info("OpenAPI spec unchanged, skipping codegen and using cached files") - cached - } - case _ => - Def.task { - s.log.info("OpenAPI spec changed, or nothing in cache: regenerating code") - zioHttpCodegenMakeTask.value - } - } - } - - cachedCodegen(FileInfo.full((ZIOpenApi / sources).value.toSet)) - }.value, - ), - ) ++ Seq( - Compile / sourceGenerators += (ZIOpenApi / zioHttpCodegenMake).taskValue, - Compile / watchSources ++= (ZIOpenApi / sources).value, - ) - - lazy val zioHttpCodegenMakeTask = Def.task { - val openApiFiles: Seq[File] = (ZIOpenApi / sources).value - val openApiRootDirs: Seq[File] = (ZIOpenApi / sourceDirectories).value - val baseDir = baseDirectory.value - val targetDir: File = (Compile / sourceManaged).value - val config: Config = (ZIOpenApi / zioHttpCodegenConf).value - - openApiFiles.flatMap { openApiFile => - val content = fileContentAsString(openApiFile) - val format = Format.fromFileName(openApiFile.getName) - val openApiRootDir = openApiRootDirs.foldLeft(baseDir) { case (bestSoFar, currentDir) => - val currentPath = currentDir.getAbsolutePath - val isAncestor = openApiFile.getAbsolutePath.startsWith(currentPath) - val isMoreSpecific = currentPath.length >= bestSoFar.getAbsolutePath.length - if (isAncestor && isMoreSpecific) currentDir - else bestSoFar - } - val parsedOrError = format match { - case Format.YAML => content.fromYaml[OpenAPI](JsonCodec.jsonDecoder(OpenAPI.schema)) - case Format.JSON => OpenAPI.fromJson(content) - } - - parsedOrError match { - case Left(error) => throw new Exception(s"Failed to parse OpenAPI from $format: $error") - case Right(openapi) => - val codegenEndpoints = EndpointGen.fromOpenAPI(openapi, config) - val basePackageParts = dirDiffToPackage(openApiRootDir, openApiFile) - val currentTargetDir = basePackageParts.foldLeft(targetDir)(_ / _) - val currentTargetPat = Path.of(currentTargetDir.toURI()) - - CodeGen - .renderedFiles(codegenEndpoints, basePackageParts.mkString(".")) - .map { case (path, content) => - val filePath = currentTargetPat.resolve(path) - Files.createDirectories(filePath.getParent) - Files.write(filePath, content.getBytes(StandardCharsets.UTF_8), CREATE, TRUNCATE_EXISTING) - filePath.toFile - } - .toSeq - - } - } - } - - private def listFilesRec(dir: File): List[File] = { - def inner(dir: File, acc: List[File]): List[File] = - sbt.io.IO.listFiles(dir).foldRight(acc) { case (f, tail) => - if (f.isDirectory) inner(f, tail) - else f :: tail - } - - inner(dir, Nil) - } - - private def fileContentAsString(file: File): String = { - val s = Source.fromFile(file) - val r = s.mkString - s.close() - r - } - - private def dirDiffToPackage(dir: File, file: File): List[String] = { - val dirPath = dir.toPath - val filePath = file.toPath - val relativePath = dirPath.relativize(filePath.getParent) - relativePath.toString.split(File.separatorChar).toList - } -} diff --git a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/build.sbt b/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/build.sbt deleted file mode 100644 index dcab846896..0000000000 --- a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/build.sbt +++ /dev/null @@ -1,44 +0,0 @@ -import gigahorse.support.apachehttp.Gigahorse -import zio.json.ast.Json -import zio.json.ast.JsonCursor -import scala.concurrent.{Await, ExecutionContext} -import scala.concurrent.duration.DurationInt -import scala.util.{Failure, Try} - -lazy val root = (project in file(".")) - .enablePlugins(ZioHttpCodegen) - .settings( - name := "uspto-sdk", - organization := "com.example", - scalaVersion := "2.13.15", - libraryDependencies +="dev.zio" %% "zio-http" % "3.0.1", - ZIOpenApi / sourceGenerators += Def.task[Seq[File]] { - - val outFile = (ZIOpenApi / sourceManaged).value / "gov" / "uspto" / "ibd" / "api.json" - val http = Gigahorse.http(Gigahorse.config) - val request = Gigahorse.url("https://developer.uspto.gov/ibd-api/v3/api-docs") - val response = http.run(request, Gigahorse.asString) - - Await.result(response.transform(_.flatMap { content => - - // TODO: this is a temporary workaround - // current zio-http-gen module has many gaps not yet implemented, - // so we need to clean the API just so we can use it here. - // in the future, once the module had matured enough, - // we should remove this part, and perhaps take a more comprehensive example like: - // https://petstore3.swagger.io - val either = for { - decodedJsObj <- Json.Obj.decoder.decodeJson(content) - noInlined404 <- decodedJsObj.delete(JsonCursor.field("paths").isObject.field("/v1/weeklyarchivedata/searchWeeklyArchiveData").isObject.field("get").isObject.field("responses").isObject.field("404")) - noInlinedAPI <- noInlined404.delete(JsonCursor.field("paths").isObject.field("/v1/weeklyarchivedata/apistatus")) - } yield noInlinedAPI - - either.fold[Try[Seq[File]]]( - failure => Failure(new Exception(failure)), - cleaned => Try { - IO.write(outFile, cleaned.toJsonPretty) - Seq(outFile) - }) - })(ExecutionContext.global), 1.minute) - } - ) \ No newline at end of file diff --git a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/project/plugins.sbt b/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/project/plugins.sbt deleted file mode 100644 index feecfbcc75..0000000000 --- a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/project/plugins.sbt +++ /dev/null @@ -1,5 +0,0 @@ -sys.props.get("plugin.version") match { - case Some(ver) => addSbtPlugin("dev.zio" % "zio-http-sbt-codegen" % ver) - case None => sys.error("""|The system property 'plugin.version' is not defined. - |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) -} \ No newline at end of file diff --git a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/test b/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/test deleted file mode 100644 index f36a96ad00..0000000000 --- a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/dynamic/test +++ /dev/null @@ -1,7 +0,0 @@ -# compile should depend on generated sources, -# thus we'll check it triggers codegen by inspecting expected .class files -# compiled from expected generated sources -> compile -$ exists target/scala-2.13/src_managed/oapi/gov/uspto/ibd/api.json -$ exists target/scala-2.13/classes/gov/uspto/ibd/v1/weeklyarchivedata/SearchWeeklyArchiveData.class -$ exists target/scala-2.13/classes/gov/uspto/ibd/component/ArchiveDataRecord.class diff --git a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/build.sbt b/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/build.sbt deleted file mode 100644 index 1233d113f0..0000000000 --- a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/build.sbt +++ /dev/null @@ -1,8 +0,0 @@ -lazy val root = (project in file(".")) - .enablePlugins(ZioHttpCodegen) - .settings( - name := "zoo-sdk", - organization := "dev.zoo", - scalaVersion := "2.13.15", - libraryDependencies +="dev.zio" %% "zio-http" % "3.0.1" - ) \ No newline at end of file diff --git a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/project/plugins.sbt b/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/project/plugins.sbt deleted file mode 100644 index feecfbcc75..0000000000 --- a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/project/plugins.sbt +++ /dev/null @@ -1,5 +0,0 @@ -sys.props.get("plugin.version") match { - case Some(ver) => addSbtPlugin("dev.zio" % "zio-http-sbt-codegen" % ver) - case None => sys.error("""|The system property 'plugin.version' is not defined. - |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) -} \ No newline at end of file diff --git a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/src/main/oapi/dev/zoo/service/openapi.yaml b/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/src/main/oapi/dev/zoo/service/openapi.yaml deleted file mode 100644 index 600b44f8d4..0000000000 --- a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/src/main/oapi/dev/zoo/service/openapi.yaml +++ /dev/null @@ -1,69 +0,0 @@ -info: - title: Animals Service - version: 0.0.1 -tags: - - name: Animals_API -paths: - /api/v1/zoo/{animal}: - get: - operationId: get_animal - parameters: - - in: path - name: animal - schema: - type: string - required: true - tags: - - Animals_API - description: Get animals by species name - responses: - "200": - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Animal' -openapi: 3.0.3 -components: - schemas: - Animal: - oneOf: - - $ref: '#/components/schemas/Alligator' - - $ref: '#/components/schemas/Zebra' - AnimalSharedFields: - type: object - required: - - age - - weight - properties: - age: - type: integer - format: int32 - minimum: 0 - weight: - type: number - format: double - minimum: 0 - Alligator: - allOf: - - $ref: '#/components/schemas/AnimalSharedFields' - - type: object - required: - - num_teeth - properties: - num_teeth: - type: integer - format: int32 - minimum: 0 - Zebra: - allOf: - - $ref: '#/components/schemas/AnimalSharedFields' - - type: object - required: - - num_stripes - properties: - num_stripes: - type: integer - format: int32 - minimum: 0 diff --git a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/test b/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/test deleted file mode 100644 index 83c430a033..0000000000 --- a/zio-http-gen-sbt-plugin/src/sbt-test/zio-http-sbt-codegen/simple/test +++ /dev/null @@ -1,6 +0,0 @@ -# compile should depend on generated sources, -# thus we'll check it triggers codegen by inspecting expected .class files -# compiled from expected generated sources -> compile -$ exists target/scala-2.13/classes/dev/zoo/service/api/v1/zoo/Animal.class -$ exists target/scala-2.13/classes/dev/zoo/service/component/Animal.class \ No newline at end of file From d8f9c6969d49c4df4cc9514321ea3a39a1c86a41 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:58:57 +0100 Subject: [PATCH 07/13] Use HashMap for literals look up (#2960) (#3277) --- .github/workflows/ci.yml | 47 ++++++++++- profiling/build.sbt | 4 +- profiling/project/build.properties | 2 +- profiling/project/plugins.sbt | 6 +- .../zio/http/benchmark/RoutesBenchmark.scala | 77 +++++++++++++++++++ .../scala/zio/http/RoutePatternSpec.scala | 3 +- .../main/scala/zio/http/codec/PathCodec.scala | 28 +++---- 7 files changed, 146 insertions(+), 21 deletions(-) create mode 100644 zio-http-benchmarks/src/main/scala/zio/http/benchmark/RoutesBenchmark.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10adabc07e..4c4203e7eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -636,6 +636,44 @@ jobs: name: Jmh_Main_ProbeContentTypeBenchmark path: Main_ProbeContentTypeBenchmark.txt + Jmh_RoutesBenchmark: + name: Jmh RoutesBenchmark + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.14] + java: [temurin@8] + runs-on: ${{ matrix.os }} + steps: + - uses: coursier/setup-action@v1 + with: + apps: sbt + + - uses: actions/checkout@v4 + with: + path: zio-http + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 11 + + - name: Benchmark_Main + id: Benchmark_Main + env: + GITHUB_TOKEN: ${{secrets.ACTIONS_PAT}} + run: | + cd zio-http + sed -i -e '$aaddSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7")' project/plugins.sbt + cat > Main_RoutesBenchmark.txt + sbt -no-colors -v "zioHttpBenchmarks/jmh:run -i 3 -wi 3 -f1 -t1 RoutesBenchmark" | grep -e "thrpt" -e "avgt" >> ../Main_RoutesBenchmark.txt + + - uses: actions/upload-artifact@v4 + with: + name: Jmh_Main_RoutesBenchmark + path: Main_RoutesBenchmark.txt + Jmh_SchemeDecodeBenchmark: name: Jmh SchemeDecodeBenchmark if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} @@ -752,7 +790,7 @@ jobs: Jmh_cache: name: Cache Jmh benchmarks - needs: [Jmh_CachedDateHeaderBenchmark, Jmh_ClientBenchmark, Jmh_CookieDecodeBenchmark, Jmh_EndpointBenchmark, Jmh_HttpCollectEval, Jmh_HttpCombineEval, Jmh_HttpNestedFlatMapEval, Jmh_HttpRouteTextPerf, Jmh_ProbeContentTypeBenchmark, Jmh_SchemeDecodeBenchmark, Jmh_ServerInboundHandlerBenchmark, Jmh_UtilBenchmark] + needs: [Jmh_CachedDateHeaderBenchmark, Jmh_ClientBenchmark, Jmh_CookieDecodeBenchmark, Jmh_EndpointBenchmark, Jmh_HttpCollectEval, Jmh_HttpCombineEval, Jmh_HttpNestedFlatMapEval, Jmh_HttpRouteTextPerf, Jmh_ProbeContentTypeBenchmark, Jmh_RoutesBenchmark, Jmh_SchemeDecodeBenchmark, Jmh_ServerInboundHandlerBenchmark, Jmh_UtilBenchmark] if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} strategy: matrix: @@ -824,6 +862,13 @@ jobs: - name: Format_Main_ProbeContentTypeBenchmark run: cat Main_ProbeContentTypeBenchmark.txt >> Main_benchmarks.txt + - uses: actions/download-artifact@v4 + with: + name: Jmh_Main_RoutesBenchmark + + - name: Format_Main_RoutesBenchmark + run: cat Main_RoutesBenchmark.txt >> Main_benchmarks.txt + - uses: actions/download-artifact@v4 with: name: Jmh_Main_SchemeDecodeBenchmark diff --git a/profiling/build.sbt b/profiling/build.sbt index 09e2e7dc58..e40b8b100a 100644 --- a/profiling/build.sbt +++ b/profiling/build.sbt @@ -1,7 +1,7 @@ name := "zio-http" version := "1.0.0" scalaVersion := "2.13.6" -lazy val zhttp = ProjectRef(file("/zio-http/zio-http-src"), "zioHttp") +lazy val zioHttp = ProjectRef(file("/zio-http/zio-http-src"), "zioHttp") lazy val root = (project in file(".")) .settings( name := "helloExample", @@ -14,4 +14,4 @@ lazy val root = (project in file(".")) oldStrategy(x) }, ) - .dependsOn(zhttp) + .dependsOn(zioHttp) diff --git a/profiling/project/build.properties b/profiling/project/build.properties index a4804cf08d..fe69360b7c 100644 --- a/profiling/project/build.properties +++ b/profiling/project/build.properties @@ -1 +1 @@ -sbt.version = 1.8.3 \ No newline at end of file +sbt.version = 1.10.7 diff --git a/profiling/project/plugins.sbt b/profiling/project/plugins.sbt index f8c617df58..5152bbc1d5 100644 --- a/profiling/project/plugins.sbt +++ b/profiling/project/plugins.sbt @@ -1,3 +1,3 @@ -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") diff --git a/zio-http-benchmarks/src/main/scala/zio/http/benchmark/RoutesBenchmark.scala b/zio-http-benchmarks/src/main/scala/zio/http/benchmark/RoutesBenchmark.scala new file mode 100644 index 0000000000..4e908d74ae --- /dev/null +++ b/zio-http-benchmarks/src/main/scala/zio/http/benchmark/RoutesBenchmark.scala @@ -0,0 +1,77 @@ +package zio.http.benchmark + +import java.util.concurrent.TimeUnit + +import scala.util.Random + +import zio.{Runtime, Unsafe, ZIO} + +import zio.http.endpoint.Endpoint +import zio.http.{Handler, Method, Request, Routes} + +import org.openjdk.jmh.annotations._ + +@State(Scope.Thread) +@BenchmarkMode(Array(Mode.Throughput)) +@OutputTimeUnit(TimeUnit.SECONDS) +class RoutesBenchmark { + + val REPEAT_N = 1000 + + val paths = ('a' to 'z').inits.map(_.mkString).toList.reverse.tail + + val routes = Routes.fromIterable(paths.map(p => Endpoint(Method.GET / p).out[Unit].implementHandler(Handler.unit))) + + val requests = paths.map(p => Request.get(p)) + + def request: Request = requests(Random.nextInt(requests.size)) + val smallDataRequests = Array.fill(REPEAT_N)(request) + + def unsafeRun[E, A](zio: ZIO[Any, E, A]): Unit = Unsafe.unsafe { implicit unsafe => + Runtime.default.unsafe + .run(zio.unit) + .getOrThrowFiberFailure() + } + + val paths2 = ('b' to 'z').inits.map(_.mkString).toList.reverse.tail + + val routes2 = Routes.fromIterable(paths2.map(p => Endpoint(Method.GET / p).out[Unit].implementHandler(Handler.unit))) + + val requests2 = requests ++ paths2.map(p => Request.get(p)) + + def request2: Request = requests2(Random.nextInt(requests2.size)) + + val smallDataRequests2 = Array.fill(REPEAT_N)(request2) + + val routes3 = Routes( + Endpoint(Method.GET / "api").out[Unit].implementHandler(Handler.unit), + Endpoint(Method.GET / "ui").out[Unit].implementHandler(Handler.unit), + ) + + val requests3 = Array.fill(REPEAT_N)(List(Request.get("api"), Request.get("ui"))(Random.nextInt(2))) + + @Benchmark + def benchmarkSmallDataZioApi(): Unit = + for (r <- smallDataRequests) routes.isDefinedAt(r) + + @Benchmark + def benchmarkSmallDataZioApi2(): Unit = + for (r <- smallDataRequests2) routes2.isDefinedAt(r) + + @Benchmark + def notFound1(): Unit = + for (_ <- 1 to REPEAT_N) { + routes.isDefinedAt(Request.get("not-found")) + } + + @Benchmark + def notFound2(): Unit = + for (_ <- 1 to REPEAT_N) { + routes2.isDefinedAt(Request.get("not-found")) + } + + @Benchmark + def benchmarkSmallDataZioApi3(): Unit = + for (r <- requests3) routes3.isDefinedAt(r) + +} diff --git a/zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala b/zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala index c59f853344..1583fbcf81 100644 --- a/zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/RoutePatternSpec.scala @@ -230,9 +230,10 @@ object RoutePatternSpec extends ZIOHttpSpec { var tree: Tree[Int] = RoutePattern.Tree.empty val pattern1 = Method.GET / "users" / "123" - val pattern2 = Method.GET / "users" / trailing + val pattern2 = Method.GET / "users" / trailing / "123" tree = tree.add(pattern2, 2) + println(tree.get(Method.GET, Path("/users/bla/123"))) tree = tree.add(pattern1, 1) assertTrue(tree.get(Method.GET, Path("/users/123")).contains(1)) diff --git a/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala index 75c7e34140..4b0c4a3b31 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/PathCodec.scala @@ -17,7 +17,7 @@ package zio.http.codec import scala.annotation.tailrec -import scala.collection.immutable.ListMap +import scala.collection.immutable.{HashMap, ListMap} import scala.collection.mutable import scala.language.implicitConversions @@ -763,7 +763,7 @@ object PathCodec { } private[http] final case class SegmentSubtree[+A]( - literals: ListMap[String, SegmentSubtree[A]], + literals: Map[String, SegmentSubtree[A]], others: ListMap[SegmentCodec[_], SegmentSubtree[A]], literalsWithCollisions: Set[String], value: Chunk[A], @@ -778,8 +778,8 @@ object PathCodec { newOthers.keys, ) SegmentSubtree( - newLiterals, - newOthers, + Map(newLiterals.toList: _*), + ListMap(newOthers.toList: _*), newLiteralCollisions, self.value ++ that.value, ) @@ -791,7 +791,7 @@ object PathCodec { def get(path: Path): Chunk[A] = get(path, 0) - private def get(path: Path, from: Int, skipLiteralsFor: Set[Int] = null): Chunk[A] = { + private def get(path: Path, from: Int, skipLiteralsFor: Set[Int] = Set.empty): Chunk[A] = { val segments = path.segments val nSegments = segments.length var subtree = self @@ -804,7 +804,10 @@ object PathCodec { val segment = segments(i) // Fast path, jump down the tree: - if ((skipLiteralsFor.eq(null) || !skipLiteralsFor.contains(i)) && subtree.literals.contains(segment)) { + if ( + subtree.literals.contains(segment) + && (subtree.literalsWithCollisions.eq(Set.empty) || !skipLiteralsFor.contains(i)) + ) { // this subtree segment have conflict with others // will try others if result was empty @@ -830,7 +833,7 @@ object PathCodec { result = subtree0.value i += matched } - case n => // Slowest fallback path. Have to to find the first predicate where the subpath returns a result + case n => // Slowest fallback path. Have to find the first predicate where the subpath returns a result val matches = Array.ofDim[Int](n) var index = 0 var nPositive = 0 @@ -886,11 +889,10 @@ object PathCodec { if (trySkipLiteralIdx.nonEmpty && result.isEmpty) { trySkipLiteralIdx = trySkipLiteralIdx.reverse - val skipLiteralsFor0 = if (skipLiteralsFor eq null) Set.empty[Int] else skipLiteralsFor while (trySkipLiteralIdx.nonEmpty && result.isEmpty) { val skipIdx = trySkipLiteralIdx.head trySkipLiteralIdx = trySkipLiteralIdx.tail - result = get(path, from, skipLiteralsFor0 + skipIdx) + result = get(path, from, skipLiteralsFor + skipIdx) } result } else result @@ -914,7 +916,7 @@ object PathCodec { object SegmentSubtree { def single[A](segments: Iterable[SegmentCodec[_]], value: A): SegmentSubtree[A] = segments.collect { case x if x.nonEmpty => x } - .foldRight[SegmentSubtree[A]](SegmentSubtree(ListMap(), ListMap(), Set.empty, Chunk(value))) { + .foldRight[SegmentSubtree[A]](SegmentSubtree(Map.empty, ListMap(), Set.empty, Chunk(value))) { case (segment, subtree) => val literals = segment match { @@ -928,14 +930,14 @@ object PathCodec { case _ => Chunk((segment, subtree)) }): _*) - SegmentSubtree(literals, others, Set.empty, Chunk.empty) + SegmentSubtree(Map(literals.toList: _*), others, Set.empty, Chunk.empty) } val empty: SegmentSubtree[Nothing] = - SegmentSubtree(ListMap(), ListMap(), Set.empty, Chunk.empty) + SegmentSubtree(Map(), ListMap(), Set.empty, Chunk.empty) } - private def mergeMaps[A, B](left: ListMap[A, B], right: ListMap[A, B])(f: (B, B) => B): ListMap[A, B] = + private def mergeMaps[A, B](left: Map[A, B], right: Map[A, B])(f: (B, B) => B): Map[A, B] = right.foldLeft(left) { case (acc, (k, v)) => acc.get(k) match { case None => acc.updated(k, v) From 2b865d1a9d229a6ff16fe12000092e67fcab05f6 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Fri, 31 Jan 2025 22:19:03 +0100 Subject: [PATCH 08/13] Update Scala and zio schema --- .github/workflows/ci.yml | 64 +++++++++++++++++++------------------- project/BuildHelper.scala | 8 ++--- project/Dependencies.scala | 2 +- project/plugins.sbt | 2 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c4203e7eb..9aef2786a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.19, 2.13.14, 3.3.3] + scala: [2.12.20, 2.13.16, 3.3.4] java: - graal_graalvm@17 - graal_graalvm@21 @@ -82,8 +82,8 @@ jobs: apps: sbt - name: Check formatting - if: matrix.scala == '2.13.14' - run: sbt ++2.13.14 fmtCheck + if: matrix.scala == '2.13.16' + run: sbt ++2.13.16 fmtCheck - name: Check that workflows are up to date run: sbt '++ ${{ matrix.scala }}' githubWorkflowCheck @@ -97,10 +97,10 @@ jobs: - name: Check doc generation if: ${{ github.event_name == 'pull_request' }} - run: sbt ++2.13.14 doc + run: sbt ++2.13.16 doc - name: zio-http-shaded Tests - if: matrix.scala == '2.13.14' + if: matrix.scala == '2.13.16' env: PUBLISH_SHADED: true run: sbt '++ ${{ matrix.scala }}' zioHttpShadedTests/test @@ -121,7 +121,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [graal_graalvm@17] runs-on: ${{ matrix.os }} steps: @@ -166,32 +166,32 @@ jobs: java-version: 21 cache: sbt - - name: Download target directories (2.12.19) + - name: Download target directories (2.12.20) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-2.12.19-${{ matrix.java }} + name: target-${{ matrix.os }}-2.12.20-${{ matrix.java }} - - name: Inflate target directories (2.12.19) + - name: Inflate target directories (2.12.20) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (2.13.14) + - name: Download target directories (2.13.16) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-2.13.14-${{ matrix.java }} + name: target-${{ matrix.os }}-2.13.16-${{ matrix.java }} - - name: Inflate target directories (2.13.14) + - name: Inflate target directories (2.13.16) run: | tar xf targets.tar rm targets.tar - - name: Download target directories (3.3.3) + - name: Download target directories (3.3.4) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-3.3.3-${{ matrix.java }} + name: target-${{ matrix.os }}-3.3.4-${{ matrix.java }} - - name: Inflate target directories (3.3.3) + - name: Inflate target directories (3.3.4) run: | tar xf targets.tar rm targets.tar @@ -264,7 +264,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -278,7 +278,7 @@ jobs: - name: Add Scoverage id: add_plugin - run: sed -i -e '$aaddSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.12")' project/plugins.sbt + run: sed -i -e '$aaddSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0")' project/plugins.sbt - name: Update Build Definition id: update_build_definition @@ -300,7 +300,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -338,7 +338,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -376,7 +376,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -414,7 +414,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -452,7 +452,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -490,7 +490,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -528,7 +528,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -566,7 +566,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -604,7 +604,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -642,7 +642,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -680,7 +680,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -718,7 +718,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -756,7 +756,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -915,7 +915,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -994,7 +994,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.14] + scala: [2.13.16] java: [temurin@8] runs-on: ${{ matrix.os }} steps: diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 44e6243a86..89b323d4ce 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -6,10 +6,10 @@ import xerial.sbt.Sonatype.autoImport.* import sbtcrossproject.CrossPlugin.autoImport.crossProjectPlatform object BuildHelper extends ScalaSettings { - val Scala212 = "2.12.19" - val Scala213 = "2.13.14" - val Scala3 = "3.3.3" - val ScoverageVersion = "2.0.12" + val Scala212 = "2.12.20" + val Scala213 = "2.13.16" + val Scala3 = "3.3.4" + val ScoverageVersion = "2.3.0" val JmhVersion = "0.4.7" private val stdOptions = Seq( diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 267b6f5c21..bcc954795d 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,7 +9,7 @@ object Dependencies { val ZioCliVersion = "0.5.0" val ZioJsonVersion = "0.7.1" val ZioParserVersion = "0.1.10" - val ZioSchemaVersion = "1.5.0" + val ZioSchemaVersion = "1.6.1" val SttpVersion = "3.3.18" val ZioConfigVersion = "4.0.2" diff --git a/project/plugins.sbt b/project/plugins.sbt index 737041f101..be939d3887 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") From 3585347144a60895612a91a5a302c26e5b0c78db Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Fri, 31 Jan 2025 22:25:12 +0100 Subject: [PATCH 09/13] Update NettyBody.scala --- zio-http/jvm/src/main/scala/zio/http/netty/NettyBody.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/NettyBody.scala b/zio-http/jvm/src/main/scala/zio/http/netty/NettyBody.scala index c5ba490efa..e66416e71d 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/NettyBody.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/NettyBody.scala @@ -161,7 +161,7 @@ object NettyBody extends BodyEncoding { lazy val loop: ZChannel[Any, Any, Any, Any, E, Chunk[A], Unit] = ZChannel.unwrap( queue.take - .flatMap(_.done) + .flatMap(_.exit) .fold( maybeError => ZChannel.fromZIO(queue.shutdown) *> From 84866cacd7b538969a94a7bc53f5cca3b8dbd7fc Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Fri, 31 Jan 2025 22:30:35 +0100 Subject: [PATCH 10/13] Update Scala js --- project/plugins.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index be939d3887..40cd8cf0c9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,10 +7,10 @@ addSbtPlugin("com.github.sbt" % "sbt-github-actions" % "0.23.0") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.4.0-alpha.26") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.12") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") addSbtPlugin("io.get-coursier" % "sbt-shading" % "2.1.4") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.7") addSbtPlugin("com.thesamet" % "sbt-protoc-gen-project" % "0.1.8") From 7ccdfbb29f5c06a623492deb609a878daa761f63 Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Sat, 1 Feb 2025 08:33:54 +1100 Subject: [PATCH 11/13] Update Netty to latest version, `4.1.117.Final` (#3279) --- project/Dependencies.scala | 2 +- .../zio/http/netty/model/Conversions.scala | 15 ++++------ .../test/scala/zio/http/ClientHttpsSpec.scala | 29 ++++++++++++------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index bcc954795d..04b82cecae 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -2,7 +2,7 @@ import sbt.* object Dependencies { val JwtCoreVersion = "10.0.1" - val NettyVersion = "4.1.116.Final" + val NettyVersion = "4.1.117.Final" val NettyIncubatorVersion = "0.0.25.Final" val ScalaCompactCollectionVersion = "2.12.0" val ZioVersion = "2.1.11" diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala b/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala index 374c9ba27f..6a35cc21ec 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala @@ -21,8 +21,7 @@ import scala.collection.AbstractIterator import zio.http.Server.Config.CompressionOptions import zio.http._ -import com.aayushatharva.brotli4j.encoder.Encoder -import io.netty.handler.codec.compression.StandardCompressionOptions +import io.netty.handler.codec.compression.{BrotliMode, StandardCompressionOptions} import io.netty.handler.codec.http._ import io.netty.handler.codec.http.websocketx.WebSocketScheme @@ -150,15 +149,13 @@ private[netty] object Conversions { case CompressionOptions.Deflate(cfg) => StandardCompressionOptions.deflate(cfg.level, cfg.bits, cfg.mem) case CompressionOptions.Brotli(cfg) => - StandardCompressionOptions.brotli( - new Encoder.Parameters().setQuality(cfg.quality).setWindow(cfg.lgwin).setMode(brotliModeToJava(cfg.mode)), - ) + StandardCompressionOptions.brotli(cfg.quality, cfg.lgwin, brotliModeToJava(cfg.mode)) } - def brotliModeToJava(brotli: CompressionOptions.Mode): Encoder.Mode = brotli match { - case CompressionOptions.Mode.Font => Encoder.Mode.FONT - case CompressionOptions.Mode.Text => Encoder.Mode.TEXT - case CompressionOptions.Mode.Generic => Encoder.Mode.GENERIC + def brotliModeToJava(brotli: CompressionOptions.Mode): BrotliMode = brotli match { + case CompressionOptions.Mode.Font => BrotliMode.FONT + case CompressionOptions.Mode.Text => BrotliMode.TEXT + case CompressionOptions.Mode.Generic => BrotliMode.GENERIC } def versionToNetty(version: Version): HttpVersion = version match { diff --git a/zio-http/jvm/src/test/scala/zio/http/ClientHttpsSpec.scala b/zio-http/jvm/src/test/scala/zio/http/ClientHttpsSpec.scala index 3e466bb4d5..77eeafa3e2 100644 --- a/zio-http/jvm/src/test/scala/zio/http/ClientHttpsSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/ClientHttpsSpec.scala @@ -18,19 +18,18 @@ package zio.http import zio._ import zio.test.Assertion._ -import zio.test.TestAspect.{flaky, ignore, nonFlaky} -import zio.test.{TestAspect, assertZIO} +import zio.test.TestAspect.{ignore, nonFlaky} +import zio.test.{Spec, TestAspect, TestEnvironment, assertZIO} import zio.http.netty.NettyConfig import zio.http.netty.client.NettyClientDriver abstract class ClientHttpsSpecBase extends ZIOHttpSpec { - val sslConfig: ClientSSLConfig - val zioDev = + private val zioDev = URL.decode("https://zio.dev").toOption.get - val badRequest = + private val badRequest = URL .decode( "https://httpbin.org/status/400", @@ -38,10 +37,10 @@ abstract class ClientHttpsSpecBase extends ZIOHttpSpec { .toOption .get - val untrusted = + private val untrusted = URL.decode("https://untrusted-root.badssl.com/").toOption.get - override def spec = suite("Https Client request")( + def tests(sslConfig: ClientSSLConfig) = suite("Client")( test("respond Ok") { val actual = Client.batched(Request.get(zioDev)) assertZIO(actual)(anything) @@ -74,7 +73,7 @@ abstract class ClientHttpsSpecBase extends ZIOHttpSpec { ), ), ) - } @@ nonFlaky(20), + }, ) .provideShared( ZLayer.succeed(ZClient.Config.default.ssl(sslConfig)), @@ -91,17 +90,27 @@ abstract class ClientHttpsSpecBase extends ZIOHttpSpec { object ClientHttpsSpec extends ClientHttpsSpecBase { - val sslConfig = ClientSSLConfig.FromTrustStoreResource( + private val sslConfig = ClientSSLConfig.FromTrustStoreResource( trustStorePath = "truststore.jks", trustStorePassword = "changeit", ) + + override def spec: Spec[TestEnvironment & Scope, Throwable] = + suite("Https Client request - From Trust Store")( + tests(sslConfig), + ) } object ClientHttpsFromJavaxNetSslSpec extends ClientHttpsSpecBase { - val sslConfig = + private val sslConfig = ClientSSLConfig.FromJavaxNetSsl .builderWithTrustManagerResource("trustStore.jks") .trustManagerPassword("changeit") .build() + + override def spec: Spec[TestEnvironment & Scope, Throwable] = + suite("Https Client request - From Javax Net Ssl")( + tests(sslConfig), + ) } From e353e5e096cbe540b5252002fae0e55acea3a0ec Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Fri, 31 Jan 2025 23:59:15 +0100 Subject: [PATCH 12/13] Deactivate broken test --- .../src/test/scala/zio/http/gen/scala/CodeGenSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala b/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala index fda70f5ff4..f380e7e31b 100644 --- a/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala +++ b/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala @@ -958,7 +958,7 @@ object CodeGenSpec extends ZIOSpecDefault { ) } } - }, + } @@ TestAspect.ignore, // broke with zio schema update test("Endpoint with normalized field names") { val openAPIString = stringFromResource("/inline_schema_weird_field_names.yaml") From 307ca54de2c664d4c4ebc8813e4b2102b668daf7 Mon Sep 17 00:00:00 2001 From: Andriy Plokhotnyuk Date: Mon, 3 Feb 2025 14:37:17 +0100 Subject: [PATCH 13/13] Fix decoding of `JsonSchema` properties from JSON (#3287) --- project/Dependencies.scala | 2 +- .../src/test/scala/zio/http/gen/scala/CodeGenSpec.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 04b82cecae..d23128fb50 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -7,7 +7,7 @@ object Dependencies { val ScalaCompactCollectionVersion = "2.12.0" val ZioVersion = "2.1.11" val ZioCliVersion = "0.5.0" - val ZioJsonVersion = "0.7.1" + val ZioJsonVersion = "0.7.12" val ZioParserVersion = "0.1.10" val ZioSchemaVersion = "1.6.1" val SttpVersion = "3.3.18" diff --git a/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala b/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala index f380e7e31b..fda70f5ff4 100644 --- a/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala +++ b/zio-http-gen/src/test/scala/zio/http/gen/scala/CodeGenSpec.scala @@ -958,7 +958,7 @@ object CodeGenSpec extends ZIOSpecDefault { ) } } - } @@ TestAspect.ignore, // broke with zio schema update + }, test("Endpoint with normalized field names") { val openAPIString = stringFromResource("/inline_schema_weird_field_names.yaml")