diff --git a/zio-http/src/main/scala/zio/http/QueryParams.scala b/zio-http/src/main/scala/zio/http/QueryParams.scala index cd9367c941..7913a40053 100644 --- a/zio-http/src/main/scala/zio/http/QueryParams.scala +++ b/zio-http/src/main/scala/zio/http/QueryParams.scala @@ -18,9 +18,9 @@ package zio.http import java.nio.charset.Charset -import zio.Chunk +import zio.{Chunk, IO, ZIO} -import zio.http.Charsets +import zio.http.codec.TextCodec import zio.http.internal.QueryParamEncoding /** @@ -88,11 +88,47 @@ final case class QueryParams(map: Map[String, Chunk[String]]) { */ def getAll(key: String): Option[Chunk[String]] = map.get(key) + /** + * Retrieves all typed query parameter values having the specified name. + */ + def getAllAs[A](key: String)(implicit codec: TextCodec[A]): Either[QueryParamsError, Chunk[A]] = + map.get(key) match { + case Some(params) => + params + .map(param => codec.decode(param).toRight(QueryParamsError.Malformed(key, param, codec))) + .partitionMap(identity) match { + case (errors, _) if errors.nonEmpty => Left(QueryParamsError.MultiMalformed(errors)) + case (_, typedParams) => Right(typedParams) + } + case None => Left(QueryParamsError.Missing(key)) + } + + /** + * 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)) + /** * Retrieves the first query parameter value having the specified name. */ def get(key: String): Option[String] = getAll(key).flatMap(_.headOption) + /** + * Retrieves the first typed query parameter value having the specified name. + */ + def getAs[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, param, codec)) + } yield typedParam + + /** + * 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)) + /** * Retrieves all query parameter values having the specified name, or else * uses the default iterable. @@ -100,6 +136,13 @@ final case class QueryParams(map: Map[String, Chunk[String]]) { def getAllOrElse(key: String, default: => Iterable[String]): Chunk[String] = getAll(key).getOrElse(Chunk.fromIterable(default)) + /** + * 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)) + /** * Retrieves the first query parameter value having the specified name, or * else uses the default value. @@ -107,6 +150,13 @@ final case class QueryParams(map: Map[String, Chunk[String]]) { def getOrElse(key: String, default: => String): String = get(key).getOrElse(default) + /** + * 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) + override def hashCode: Int = normalize.map.hashCode /** diff --git a/zio-http/src/main/scala/zio/http/QueryParamsError.scala b/zio-http/src/main/scala/zio/http/QueryParamsError.scala new file mode 100644 index 0000000000..4ff65ab9a7 --- /dev/null +++ b/zio-http/src/main/scala/zio/http/QueryParamsError.scala @@ -0,0 +1,44 @@ +/* + * 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 java.nio.charset.Charset + +import scala.util.control.NoStackTrace + +import zio.Chunk + +import zio.http.codec.TextCodec +import zio.http.internal.QueryParamEncoding + +sealed trait QueryParamsError extends Exception with NoStackTrace { + override def getMessage(): String = message + def message: String +} +object QueryParamsError { + final case class Missing(queryParamName: String) extends QueryParamsError { + def message = s"Missing query parameter with name $queryParamName" + } + + final case class Malformed(name: String, value: String, codec: TextCodec[_]) extends QueryParamsError { + def message = s"Unable to decode query parameter with name $name and value $value using $codec" + } + + final case class MultiMalformed(chunk: Chunk[Malformed]) extends QueryParamsError { + def message: String = chunk.map(_.getMessage()).mkString("; ") + } +} diff --git a/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala b/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala index 277ace21ed..7d7f551427 100644 --- a/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala +++ b/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala @@ -16,7 +16,7 @@ package zio.http -import zio.test.Assertion.equalTo +import zio.test.Assertion.{anything, equalTo, fails, hasSize} import zio.test._ import zio.{Chunk, ZIO} @@ -236,10 +236,45 @@ object QueryParamsSpec extends ZIOHttpSpec { val default = "default" val unknown = "non-existent" val queryParams = QueryParams(name -> "a", name -> "b") - assertTrue(queryParams.get(name).get == "a") && - assertTrue(queryParams.getOrElse(unknown, default) == default) && - assertTrue(queryParams.getAll(name).get.length == 2) && - assertTrue(queryParams.getAllOrElse(unknown, Chunk(default)).length == 1) + assertTrue( + queryParams.get(name).get == "a", + queryParams.get(unknown).isEmpty, + queryParams.getOrElse(name, default) == "a", + queryParams.getOrElse(unknown, default) == default, + queryParams.getAll(name).get.length == 2, + queryParams.getAll(unknown).isEmpty, + queryParams.getAllOrElse(name, Chunk(default)).length == 2, + queryParams.getAllOrElse(unknown, Chunk(default)).length == 1, + ) + }, + ), + suite("getAs - getAllAs")( + test("success") { + val typed = "typed" + val default = 3 + val invalidTyped = "invalidTyped" + 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, + ) + 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)) }, ), suite("encode - decode")(