diff --git a/zio-http-cli/src/main/scala/zio/http/endpoint/cli/CliEndpoint.scala b/zio-http-cli/src/main/scala/zio/http/endpoint/cli/CliEndpoint.scala index dffbc04aa1..edb381c492 100644 --- a/zio-http-cli/src/main/scala/zio/http/endpoint/cli/CliEndpoint.scala +++ b/zio-http-cli/src/main/scala/zio/http/endpoint/cli/CliEndpoint.scala @@ -123,7 +123,7 @@ private[cli] object CliEndpoint { case HttpCodec.Path(pathCodec, _) => CliEndpoint(url = HttpOptions.Path(pathCodec) :: List()) - case HttpCodec.Query(name, textCodec, _) => + case HttpCodec.Query(name, textCodec, _, _) => textCodec.asInstanceOf[TextCodec[_]] match { case TextCodec.Constant(value) => CliEndpoint(url = HttpOptions.QueryConstant(name, value) :: List()) case _ => CliEndpoint(url = HttpOptions.Query(name, textCodec) :: List()) diff --git a/zio-http-cli/src/test/scala/zio/http/endpoint/cli/EndpointGen.scala b/zio-http-cli/src/test/scala/zio/http/endpoint/cli/EndpointGen.scala index 5c7ba70f94..67db3627a6 100644 --- a/zio-http-cli/src/test/scala/zio/http/endpoint/cli/EndpointGen.scala +++ b/zio-http-cli/src/test/scala/zio/http/endpoint/cli/EndpointGen.scala @@ -7,6 +7,7 @@ import zio.test._ import zio.schema._ import zio.http._ +import zio.http.codec.HttpCodec.Query.QueryParamHint import zio.http.codec._ import zio.http.endpoint._ import zio.http.endpoint.cli.AuxGen._ @@ -93,7 +94,7 @@ object EndpointGen { lazy val anyQuery: Gen[Any, CliReprOf[Codec[_]]] = Gen.alphaNumericStringBounded(1, 30).zip(anyTextCodec).map { case (name, codec) => CliRepr( - HttpCodec.Query(name, codec), + HttpCodec.Query(name, codec, QueryParamHint.Any), codec match { case TextCodec.Constant(value) => CliEndpoint(url = HttpOptions.QueryConstant(name, value) :: Nil) case _ => CliEndpoint(url = HttpOptions.Query(name, codec) :: Nil) diff --git a/zio-http/shared/src/main/scala/zio/http/codec/HttpCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/HttpCodec.scala index f858bc4238..d9a6a6c7b6 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/HttpCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/HttpCodec.scala @@ -553,7 +553,7 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with } private[http] final case class Status[A](codec: SimpleCodec[zio.http.Status, A], index: Int = 0) - extends Atom[HttpCodecType.Status, A] { + extends Atom[HttpCodecType.Status, A] { self => def erase: Status[Any] = self.asInstanceOf[Status[Any]] @@ -590,8 +590,12 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with def index(index: Int): ContentStream[A] = copy(index = index) } - private[http] final case class Query[A](name: String, textCodec: TextCodec[A], index: Int = 0) - extends Atom[HttpCodecType.Query, Chunk[A]] { + private[http] final case class Query[A]( + name: String, + textCodec: TextCodec[A], + hint: Query.QueryParamHint, + index: Int = 0, + ) extends Atom[HttpCodecType.Query, Chunk[A]] { self => def erase: Query[Any] = self.asInstanceOf[Query[Any]] @@ -600,6 +604,21 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with def index(index: Int): Query[A] = copy(index = index) } + private[http] object Query { + + // Hint on how many query parameters codec expects + sealed trait QueryParamHint + object QueryParamHint { + case object One extends QueryParamHint + + case object Many extends QueryParamHint + + case object Zero extends QueryParamHint + + case object Any extends QueryParamHint + } + } + private[http] final case class Method[A](codec: SimpleCodec[zio.http.Method, A], index: Int = 0) extends Atom[HttpCodecType.Method, A] { self => diff --git a/zio-http/shared/src/main/scala/zio/http/codec/QueryCodecs.scala b/zio-http/shared/src/main/scala/zio/http/codec/QueryCodecs.scala index 98c6403346..7a07f78cba 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/QueryCodecs.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/QueryCodecs.scala @@ -15,38 +15,37 @@ */ package zio.http.codec +import zio.Chunk import zio.stacktracer.TracingImplicits.disableAutoTrace -import zio.{Chunk, NonEmptyChunk} + +import zio.http.codec.HttpCodec.Query.QueryParamHint private[codec] trait QueryCodecs { - def query(name: String): QueryCodec[String] = - toSingleValue(name, HttpCodec.Query(name, TextCodec.string)) + def query(name: String): QueryCodec[String] = singleValueCodec(name, TextCodec.string) + + def queryBool(name: String): QueryCodec[Boolean] = singleValueCodec(name, TextCodec.boolean) - def queryBool(name: String): QueryCodec[Boolean] = - toSingleValue(name, HttpCodec.Query(name, TextCodec.boolean)) + def queryInt(name: String): QueryCodec[Int] = singleValueCodec(name, TextCodec.int) - def queryInt(name: String): QueryCodec[Int] = - toSingleValue(name, HttpCodec.Query(name, TextCodec.int)) + def queryTo[A](name: String)(implicit codec: TextCodec[A]): QueryCodec[A] = singleValueCodec(name, codec) - def queryTo[A](name: String)(implicit codec: TextCodec[A]): QueryCodec[A] = - toSingleValue(name, HttpCodec.Query(name, codec)) + def queryAll(name: String): QueryCodec[Chunk[String]] = multiValueCodec(name, TextCodec.string) - def queryAll(name: String): QueryCodec[Chunk[String]] = - HttpCodec.Query(name, TextCodec.string) + def queryAllBool(name: String): QueryCodec[Chunk[Boolean]] = multiValueCodec(name, TextCodec.boolean) - def queryAllBool(name: String): QueryCodec[Chunk[Boolean]] = - HttpCodec.Query(name, TextCodec.boolean) + def queryAllInt(name: String): QueryCodec[Chunk[Int]] = multiValueCodec(name, TextCodec.int) - def queryAllInt(name: String): QueryCodec[Chunk[Int]] = - HttpCodec.Query(name, TextCodec.int) + def queryAllTo[A](name: String)(implicit codec: TextCodec[A]): QueryCodec[Chunk[A]] = multiValueCodec(name, codec) - def queryAllTo[A](name: String)(implicit codec: TextCodec[A]): QueryCodec[Chunk[A]] = - HttpCodec.Query(name, codec) + private def singleValueCodec[A](name: String, textCodec: TextCodec[A]): QueryCodec[A] = + HttpCodec + .Query(name, textCodec, QueryParamHint.One) + .transformOrFail { + case chunk if chunk.size == 1 => Right(chunk.head) + case chunk => Left(s"Expected single value for query parameter $name, but got ${chunk.size} instead") + }(s => Right(Chunk(s))) - private def toSingleValue[A](name: String, queryCodec: QueryCodec[Chunk[A]]): QueryCodec[A] = - queryCodec.transformOrFail { - case chunk if chunk.size == 1 => Right(chunk.head) - case chunk => Left(s"Expected single value for query parameter $name, but got ${chunk.size} instead") - }(s => Right(Chunk(s))) + private def multiValueCodec[A](name: String, textCodec: TextCodec[A]): QueryCodec[Chunk[A]] = + HttpCodec.Query(name, textCodec, QueryParamHint.Many) } 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 9a28077a54..66e586910c 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 @@ -601,7 +601,7 @@ object OpenAPIGen { queryParams ++ pathParams ++ headerParams def queryParams: Set[OpenAPI.ReferenceOr[OpenAPI.Parameter]] = { - inAtoms.query.collect { case mc @ MetaCodec(HttpCodec.Query(name, codec, _), _) => + inAtoms.query.collect { case mc @ MetaCodec(HttpCodec.Query(name, codec, _, _), _) => OpenAPI.ReferenceOr.Or( OpenAPI.Parameter.queryParameter( name = name,