Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schema based ops for en- and decoding bodies #2569

Merged
merged 2 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/migration/RC4-to-xx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**QueryCodec**
- removed methods that start with `param` use methods starting with `query` instead
- renamed `queryAs` to `queryTo`

**QueryParam**
- renamed all methods that return typed params from `as` to `to`
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ trait TestCliEndpoints {
"posts" / int("postId") ?? Doc.p("The unique identifier of the post"),
)
.query(
paramStr("user-name") ?? Doc.p(
query("user-name") ?? Doc.p(
"The user's name",
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import zio.http.codec._

object CombinerTypesExample extends App {

val foo = paramStr("foo")
val bar = paramStr("bar")
val foo = query("foo")
val bar = query("bar")

val combine1L1R: HttpCodec[HttpCodecType.Query, (String, String)] = foo & bar
val combine1L2R: HttpCodec[HttpCodecType.Query, (String, String, String)] = foo & (bar & bar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ object CodeGen {
case Code.CodecType.UUID => "UUID"
case Code.CodecType.Literal => throw new Exception("Literal query params are not supported")
}
s""".query(QueryCodec.queryAs[$tpe]("$name"))"""
s""".query(QueryCodec.queryTo[$tpe]("$name"))"""
}

def renderInCode(inCode: Code.InCode): String = inCode match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ object Users {
import zio.http.endpoint._
import zio.http.codec._
val get = Endpoint(Method.GET / "api" / "v1" / "users")
.query(QueryCodec.queryAs[Int]("limit"))
.query(QueryCodec.queryAs[String]("name"))
.query(QueryCodec.queryTo[Int]("limit"))
.query(QueryCodec.queryTo[String]("name"))
.in[Unit]

}
49 changes: 49 additions & 0 deletions zio-http/src/main/scala/zio/http/Body.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import zio.stacktracer.TracingImplicits.disableAutoTrace

import zio.stream.ZStream

import zio.schema.codec.BinaryCodec

import zio.http.internal.BodyEncoding

/**
Expand All @@ -41,6 +43,23 @@ trait Body { self =>
*/
def ++(that: Body): Body = if (that.isEmpty) self else that

/**
* Decodes the content of the body as a value based on a zio-schema
* [[zio.schema.codec.BinaryCodec]].<br>
*
* Example for json:
* {{{
* import zio.schema.json.codec._
* case class Person(name: String, age: Int)
* implicit val schema: Schema[Person] = DeriveSchema.gen[Person]
* val person = Person("John", 42)
* val body = Body.from(person)
* val decodedPerson = body.to[Person]
* }}}
*/
def to[A](implicit codec: BinaryCodec[A], trace: Trace): Task[A] =
asChunk.flatMap(bytes => ZIO.fromEither(codec.decode(bytes)))

/**
* Returns an effect that decodes the content of the body as array of bytes.
* Note that attempting to decode a large stream of bytes into an array could
Expand Down Expand Up @@ -155,6 +174,20 @@ object Body {
*/
val empty: Body = EmptyBody

/**
* Constructs a [[zio.http.Body]] from a value based on a zio-schema
* [[zio.schema.codec.BinaryCodec]].<br> Example for json:
* {{{
* import zio.schema.codec.JsonCodec._
* case class Person(name: String, age: Int)
* implicit val schema: Schema[Person] = DeriveSchema.gen[Person]
* val person = Person("John", 42)
* val body = Body.from(person)
* }}}
*/
def from[A](a: A)(implicit codec: BinaryCodec[A], trace: Trace): Body =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that this is called from almost does imply to or into.

fromChunk(codec.encode(a))

/**
* Constructs a [[zio.http.Body]] from the contents of a file.
*/
Expand Down Expand Up @@ -215,6 +248,22 @@ object Body {
def fromStream(stream: ZStream[Any, Throwable, Byte], contentLength: Long): Body =
StreamBody(stream, knownContentLength = Some(contentLength))

/**
* Constructs a [[zio.http.Body]] from stream of values based on a zio-schema
* [[zio.schema.codec.BinaryCodec]].<br>
*
* Example for json:
* {{{
* import zio.schema.codec.JsonCodec._
* case class Person(name: String, age: Int)
* implicit val schema: Schema[Person] = DeriveSchema.gen[Person]
* val persons = ZStream(Person("John", 42))
* val body = Body.fromStream(persons)
* }}}
*/
def fromStream[A](stream: ZStream[Any, Throwable, A])(implicit codec: BinaryCodec[A], trace: Trace): Body =
StreamBody(stream >>> codec.streamEncoder, knownContentLength = None)

/**
* Constructs a [[zio.http.Body]] from a stream of bytes of unknown length,
* using chunked transfer encoding.
Expand Down
18 changes: 9 additions & 9 deletions zio-http/src/main/scala/zio/http/QueryParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ final case class QueryParams(map: Map[String, Chunk[String]]) {
/**
* Retrieves all typed query parameter values having the specified name.
*/
def getAllAs[A](key: String)(implicit codec: TextCodec[A]): Either[QueryParamsError, Chunk[A]] = for {
def getAllTo[A](key: String)(implicit codec: TextCodec[A]): Either[QueryParamsError, Chunk[A]] = for {
params <- map.get(key).toRight(QueryParamsError.Missing(key))
(failed, typed) = params.partitionMap(p => codec.decode(p).toRight(p))
result <- NonEmptyChunk
Expand All @@ -104,8 +104,8 @@ final case class QueryParams(map: Map[String, Chunk[String]]) {
* Retrieves all typed query parameter values having the specified name as
* ZIO.
*/
def getAllAsZIO[A](key: String)(implicit codec: TextCodec[A]): IO[QueryParamsError, Chunk[A]] =
ZIO.fromEither(getAllAs[A](key))
def getAllToZIO[A](key: String)(implicit codec: TextCodec[A]): IO[QueryParamsError, Chunk[A]] =
ZIO.fromEither(getAllTo[A](key))

/**
* Retrieves the first query parameter value having the specified name.
Expand All @@ -115,7 +115,7 @@ final case class QueryParams(map: Map[String, Chunk[String]]) {
/**
* Retrieves the first typed query parameter value having the specified name.
*/
def getAs[A](key: String)(implicit codec: TextCodec[A]): Either[QueryParamsError, A] = for {
def getTo[A](key: String)(implicit codec: TextCodec[A]): Either[QueryParamsError, A] = for {
param <- get(key).toRight(QueryParamsError.Missing(key))
typedParam <- codec.decode(param).toRight(QueryParamsError.Malformed(key, codec, NonEmptyChunk(param)))
} yield typedParam
Expand All @@ -124,7 +124,7 @@ final case class QueryParams(map: Map[String, Chunk[String]]) {
* Retrieves the first typed query parameter value having the specified name
* as ZIO.
*/
def getAsZIO[A](key: String)(implicit codec: TextCodec[A]): IO[QueryParamsError, A] = ZIO.fromEither(getAs[A](key))
def getToZIO[A](key: String)(implicit codec: TextCodec[A]): IO[QueryParamsError, A] = ZIO.fromEither(getTo[A](key))

/**
* Retrieves all query parameter values having the specified name, or else
Expand All @@ -137,8 +137,8 @@ final case class QueryParams(map: Map[String, Chunk[String]]) {
* Retrieves all query parameter values having the specified name, or else
* uses the default iterable.
*/
def getAllAsOrElse[A](key: String, default: => Iterable[A])(implicit codec: TextCodec[A]): Chunk[A] =
getAllAs[A](key).getOrElse(Chunk.fromIterable(default))
def getAllToOrElse[A](key: String, default: => Iterable[A])(implicit codec: TextCodec[A]): Chunk[A] =
getAllTo[A](key).getOrElse(Chunk.fromIterable(default))

/**
* Retrieves the first query parameter value having the specified name, or
Expand All @@ -151,8 +151,8 @@ final case class QueryParams(map: Map[String, Chunk[String]]) {
* Retrieves the first typed query parameter value having the specified name,
* or else uses the default value.
*/
def getAsOrElse[A](key: String, default: => A)(implicit codec: TextCodec[A]): A =
getAs[A](key).getOrElse(default)
def getToOrElse[A](key: String, default: => A)(implicit codec: TextCodec[A]): A =
getTo[A](key).getOrElse(default)

override def hashCode: Int = normalize.map.hashCode

Expand Down
15 changes: 2 additions & 13 deletions zio-http/src/main/scala/zio/http/codec/QueryCodecs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package zio.http.codec
import zio.stacktracer.TracingImplicits.disableAutoTrace

private[codec] trait QueryCodecs {
def query(name: String): QueryCodec[String] =
HttpCodec.Query(name, TextCodec.string)
Expand All @@ -26,19 +27,7 @@ private[codec] trait QueryCodecs {
def queryInt(name: String): QueryCodec[Int] =
HttpCodec.Query(name, TextCodec.int)

def queryAs[A](name: String)(implicit codec: TextCodec[A]): QueryCodec[A] =
HttpCodec.Query(name, codec)

def paramStr(name: String): QueryCodec[String] =
HttpCodec.Query(name, TextCodec.string)

def paramBool(name: String): QueryCodec[Boolean] =
HttpCodec.Query(name, TextCodec.boolean)

def paramInt(name: String): QueryCodec[Int] =
HttpCodec.Query(name, TextCodec.int)

def paramAs[A](name: String)(implicit codec: TextCodec[A]): QueryCodec[A] =
def queryTo[A](name: String)(implicit codec: TextCodec[A]): QueryCodec[A] =
HttpCodec.Query(name, codec)

}
50 changes: 50 additions & 0 deletions zio-http/src/test/scala/zio/http/BodySchemaOpsSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
*
* 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 zio.http

import zio.test._

import zio.stream.ZStream

import zio.schema._
import zio.schema.codec.JsonCodec._

object BodySchemaOpsSpec extends ZIOHttpSpec {

case class Person(name: String, age: Int)
implicit val schema: Schema[Person] = DeriveSchema.gen[Person]
val person: Person = Person("John", 42)
val person2: Person = Person("Jane", 43)
val persons: ZStream[Any, Nothing, Person] = ZStream(person, person2)

def spec = suite("Body schema ops")(
test("Body.from") {
val body = Body.from(person)
val expected = """{"name":"John","age":42}"""
body.asString.map(s => assertTrue(s == expected))
},
test("Body.fromStream") {
val body = Body.fromStream(persons)
val expected = """{"name":"John","age":42}{"name":"Jane","age":43}"""
body.asString.map(s => assertTrue(s == expected))
},
test("Body#to") {
val body = Body.fromString("""{"name":"John","age":42}""")
body.to[Person].map(p => assertTrue(p == person))
},
)
}
36 changes: 18 additions & 18 deletions zio-http/src/test/scala/zio/http/QueryParamsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -256,25 +256,25 @@ object QueryParamsSpec extends ZIOHttpSpec {
val unknown = "non-existent"
val queryParams = QueryParams(typed -> "1", typed -> "2", invalidTyped -> "str")
assertTrue(
queryParams.getAs[Int](typed) == Right(1),
queryParams.getAs[Int](invalidTyped).isLeft,
queryParams.getAs[Int](unknown).isLeft,
queryParams.getAsOrElse[Int](typed, default) == 1,
queryParams.getAsOrElse[Int](invalidTyped, default) == default,
queryParams.getAsOrElse[Int](unknown, default) == default,
queryParams.getAllAs[Int](typed).map(_.length) == Right(2),
queryParams.getAllAs[Int](invalidTyped).isLeft,
queryParams.getAllAs[Int](unknown).isLeft,
queryParams.getAllAsOrElse[Int](typed, Chunk(default)).length == 2,
queryParams.getAllAsOrElse[Int](invalidTyped, Chunk(default)).length == 1,
queryParams.getAllAsOrElse[Int](unknown, Chunk(default)).length == 1,
queryParams.getTo[Int](typed) == Right(1),
queryParams.getTo[Int](invalidTyped).isLeft,
queryParams.getTo[Int](unknown).isLeft,
queryParams.getToOrElse[Int](typed, default) == 1,
queryParams.getToOrElse[Int](invalidTyped, default) == default,
queryParams.getToOrElse[Int](unknown, default) == default,
queryParams.getAllTo[Int](typed).map(_.length) == Right(2),
queryParams.getAllTo[Int](invalidTyped).isLeft,
queryParams.getAllTo[Int](unknown).isLeft,
queryParams.getAllToOrElse[Int](typed, Chunk(default)).length == 2,
queryParams.getAllToOrElse[Int](invalidTyped, Chunk(default)).length == 1,
queryParams.getAllToOrElse[Int](unknown, Chunk(default)).length == 1,
)
assertZIO(queryParams.getAsZIO[Int](typed))(equalTo(1)) &&
assertZIO(queryParams.getAsZIO[Int](invalidTyped).exit)(fails(anything)) &&
assertZIO(queryParams.getAsZIO[Int](unknown).exit)(fails(anything)) &&
assertZIO(queryParams.getAllAsZIO[Int](typed))(hasSize(equalTo(2))) &&
assertZIO(queryParams.getAllAsZIO[Int](invalidTyped).exit)(fails(anything)) &&
assertZIO(queryParams.getAllAsZIO[Int](unknown).exit)(fails(anything))
assertZIO(queryParams.getToZIO[Int](typed))(equalTo(1)) &&
assertZIO(queryParams.getToZIO[Int](invalidTyped).exit)(fails(anything)) &&
assertZIO(queryParams.getToZIO[Int](unknown).exit)(fails(anything)) &&
assertZIO(queryParams.getAllToZIO[Int](typed))(hasSize(equalTo(2))) &&
assertZIO(queryParams.getAllToZIO[Int](invalidTyped).exit)(fails(anything)) &&
assertZIO(queryParams.getAllToZIO[Int](unknown).exit)(fails(anything))
},
),
suite("encode - decode")(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object HttpCodecSpec extends ZIOHttpSpec {
val emptyJson = Body.fromString("{}")

val isAge = "isAge"
val codecBool = QueryCodec.paramBool(isAge)
val codecBool = QueryCodec.queryBool(isAge)
def makeRequest(paramValue: String) = Request.get(googleUrl.queryParams(QueryParams(isAge -> paramValue)))

def spec = suite("HttpCodecSpec")(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
private val queryParamEndpoint =
Endpoint(GET / "withQuery")
.in[SimpleInputBody]
.query(QueryCodec.paramStr("query"))
.query(QueryCodec.query("query"))
.out[SimpleOutputBody]
.outError[NotFoundError](Status.NotFound)

Expand Down Expand Up @@ -891,7 +891,7 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
HttpCodec
.content[SimpleInputBody] ?? Doc.p("simple input"),
)
.query(QueryCodec.paramStr("query"))
.query(QueryCodec.query("query"))
.outCodec(
HttpCodec
.content[SimpleOutputBody] ?? Doc.p("simple output") |
Expand Down
Loading