Skip to content

Commit

Permalink
Changes the default Endpoint.outStream[X] encoding to produce a JSON …
Browse files Browse the repository at this point in the history
…array (#3122)

Co-authored-by: Aleksandr Klimov <[email protected]>
Co-authored-by: kyri-petrou <[email protected]>
Co-authored-by: Nabil Abdel-Hafeez <[email protected]>
Co-authored-by: Naftoli Gugenheim <[email protected]>
  • Loading branch information
5 people authored Sep 20, 2024
1 parent 4764c98 commit 502331c
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 10 deletions.
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ val _ = sys.props += ("ZIOHttpLogLevel" -> Debug.ZIOHttpLogLevel)
ThisBuild / githubWorkflowEnv += ("JDK_JAVA_OPTIONS" -> "-Xms4G -Xmx8G -XX:+UseG1GC -Xss10M -XX:ReservedCodeCacheSize=1G -XX:NonProfiledCodeHeapSize=512m -Dfile.encoding=UTF-8")
ThisBuild / githubWorkflowEnv += ("SBT_OPTS" -> "-Xms4G -Xmx8G -XX:+UseG1GC -Xss10M -XX:ReservedCodeCacheSize=1G -XX:NonProfiledCodeHeapSize=512m -Dfile.encoding=UTF-8")

ThisBuild / resolvers ++= Resolver.sonatypeOssRepos("snapshots")

ThisBuild / githubWorkflowJavaVersions := Seq(
JavaSpec.graalvm(Graalvm.Distribution("graalvm"), "17"),
JavaSpec.graalvm(Graalvm.Distribution("graalvm"), "21"),
Expand Down
2 changes: 1 addition & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ object Dependencies {
val ZioCliVersion = "0.5.0"
val ZioJsonVersion = "0.7.1"
val ZioParserVersion = "0.1.10"
val ZioSchemaVersion = "1.4.1"
val ZioSchemaVersion = "1.5.0"
val SttpVersion = "3.3.18"
val ZioConfigVersion = "4.0.2"

Expand Down
4 changes: 3 additions & 1 deletion zio-http/jvm/src/test/scala/zio/http/BodySchemaOpsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ object BodySchemaOpsSpec extends ZIOHttpSpec {
},
test("Body.fromStream") {
val body = Body.fromStream(persons)
val expected = """{"name":"John","age":42}{"name":"Jane","age":43}"""
val expected =
"""{"name":"John","age":42}
|{"name":"Jane","age":43}""".stripMargin
body.asString.map(s => assertTrue(s == expected))
},
test("Body#to") {
Expand Down
37 changes: 37 additions & 0 deletions zio-http/jvm/src/test/scala/zio/http/endpoint/RoundtripSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,43 @@ object RoundtripSpec extends ZIOHttpSpec {
(stream: ZStream[Any, Nothing, Byte]) => stream.runCount.map(c => assert(c)(equalTo(1024L * 1024L))),
)
},
test("string stream output") {
val api = Endpoint(GET / "download").query(HttpCodec.query[Int]("count")).outStream[String]
val route = api.implementHandler {
Handler.fromFunctionZIO { count =>
ZIO.succeed(ZStream.fromIterable((0 until count).map(_.toString)))
}
}

testEndpointZIO(
api,
Routes(route),
1024 * 1024,
(stream: ZStream[Any, Nothing, String]) =>
stream.zipWithIndex
.runFold((true, 0)) { case ((allOk, count), (str, idx)) =>
(allOk && str == idx.toString, count + 1)
}
.map { case (allOk, c) =>
assertTrue(allOk && c == 1024 * 1024)
},
)
},
test("string output") {
val api = Endpoint(GET / "download").query(HttpCodec.query[String]("param")).out[String]
val route = api.implementHandler {
Handler.fromFunctionZIO { param =>
ZIO.succeed(param)
}
}

testEndpointZIO(
api,
Routes(route),
"test",
(str: String) => assertTrue(str == "test"),
)
},
test("multi-part input") {
val api = Endpoint(POST / "test")
.in[String]("name")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import zio.schema.{DeriveSchema, Schema}
import zio.http.Method.{GET, POST}
import zio.http._
import zio.http.codec.PathCodec.string
import zio.http.codec.{ContentCodec, Doc, HttpCodec, HttpContentCodec, QueryCodec}
import zio.http.codec.{ContentCodec, Doc, HttpCodec}
import zio.http.endpoint._

object OpenAPIGenSpec extends ZIOSpecDefault {
Expand Down Expand Up @@ -2854,6 +2854,119 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
|""".stripMargin
assertTrue(json == toJsonAst(expectedJson))
},
test("Stream schema") {
val endpoint = Endpoint(RoutePattern.POST / "folder")
.outStream[Int]
val openApi = OpenAPIGen.fromEndpoints(endpoint)
val json = toJsonAst(openApi)
val expectedJson =
"""
|{
| "openapi" : "3.1.0",
| "info" : {
| "title" : "",
| "version" : ""
| },
| "paths" : {
| "/folder" : {
| "post" : {
| "responses" : {
| "200" :
| {
| "content" : {
| "application/json" : {
| "schema" :
| {
| "type" :
| "array",
| "items" : {
| "type" :
| "integer",
| "format" : "int32"
| }
| }
| }
| }
| }
| }
| }
| }
| },
| "components" : {
|
| }
|}
|""".stripMargin
assertTrue(json == toJsonAst(expectedJson))
},
test("Stream schema multipart") {
val endpoint = Endpoint(RoutePattern.POST / "folder")
.outCodec(
HttpCodec.contentStream[String]("strings") ++
HttpCodec.contentStream[Int]("ints"),
)
val openApi = OpenAPIGen.fromEndpoints(endpoint)
val json = toJsonAst(openApi)
val expectedJson =
"""
|{
| "openapi" : "3.1.0",
| "info" : {
| "title" : "",
| "version" : ""
| },
| "paths" : {
| "/folder" : {
| "post" : {
| "responses" : {
| "default" :
| {
| "content" : {
| "multipart/form-data" : {
| "schema" :
| {
| "type" :
| "object",
| "properties" : {
| "strings" : {
| "type" :
| "array",
| "items" : {
| "type" :
| "string"
| }
| },
| "ints" : {
| "type" :
| "array",
| "items" : {
| "type" :
| "integer",
| "format" : "int32"
| }
| }
| },
| "additionalProperties" :
| false,
| "required" : [
| "strings",
| "ints"
| ]
| }
| }
| }
| }
| }
| }
| }
| },
| "components" : {
|
| }
|}
|""".stripMargin
assertTrue(json == toJsonAst(expectedJson))
},
test("Lazy schema") {
val endpoint = Endpoint(RoutePattern.POST / "lazy")
.in[Lazy.A]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package zio.http.codec

import java.nio.charset.StandardCharsets

import scala.collection.immutable.ListMap

import zio._

import zio.stream.ZPipeline
import zio.stream.{ZChannel, ZPipeline}

import zio.schema.codec.DecodeError.ReadError
import zio.schema.codec.JsonCodec.{JsonDecoder, JsonEncoder}
import zio.schema.codec._
import zio.schema.{DeriveSchema, Schema}

Expand Down Expand Up @@ -273,6 +277,7 @@ object HttpContentCodec {
}

object json {

private var jsonCodecCache: Map[Schema[_], HttpContentCodec[_]] = Map.empty
def only[A](implicit schema: Schema[A]): HttpContentCodec[A] =
if (jsonCodecCache.contains(schema)) {
Expand All @@ -282,10 +287,12 @@ object HttpContentCodec {
ListMap(
MediaType.application.`json` ->
BinaryCodecWithSchema(
config =>
JsonCodec.schemaBasedBinaryCodec[A](
JsonCodec.Config(ignoreEmptyCollections = config.ignoreEmptyCollections),
)(schema),
config => {
JsonCodec.schemaBasedBinaryCodec(
JsonCodec
.Config(ignoreEmptyCollections = config.ignoreEmptyCollections, treatStreamsAsArrays = true),
)(schema)
},
schema,
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,17 @@ object OpenAPIGen {
findName(metadata).orElse(maybeName).getOrElse(throw new Exception("Multipart content without name"))
JsonSchema.obj(
name -> JsonSchema
.fromZSchema(codec.lookup(mediaType).map(_._2.schema).getOrElse(codec.defaultSchema), referenceType)
.ArrayType(
Some(
JsonSchema
.fromZSchema(
codec.lookup(mediaType).map(_._2.schema).getOrElse(codec.defaultSchema),
referenceType,
),
),
None,
uniqueItems = false,
)
.description(descriptionFromMeta)
.deprecated(deprecated(metadata))
.nullable(optional(metadata)),
Expand All @@ -327,7 +337,14 @@ object OpenAPIGen {
.nullable(optional(metadata))
case HttpCodec.ContentStream(codec, _, _) =>
JsonSchema
.fromZSchema(codec.lookup(mediaType).map(_._2.schema).getOrElse(codec.defaultSchema), referenceType)
.ArrayType(
Some(
JsonSchema
.fromZSchema(codec.lookup(mediaType).map(_._2.schema).getOrElse(codec.defaultSchema), referenceType),
),
None,
uniqueItems = false,
)
.description(descriptionFromMeta)
.deprecated(deprecated(metadata))
.nullable(optional(metadata))
Expand Down

0 comments on commit 502331c

Please sign in to comment.