From 17a45fab93f7fb5286023c5b3a7b015d01826b77 Mon Sep 17 00:00:00 2001 From: Maciej Bany Date: Wed, 1 Nov 2023 14:17:08 +0100 Subject: [PATCH 1/5] added getAs[A] for QueryParams --- .../src/main/scala/zio/http/QueryParams.scala | 26 +++++++++++++++++ .../test/scala/zio/http/QueryParamsSpec.scala | 29 ++++++++++++++----- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/QueryParams.scala b/zio-http/src/main/scala/zio/http/QueryParams.scala index cd9367c941..1a9d63ac7c 100644 --- a/zio-http/src/main/scala/zio/http/QueryParams.scala +++ b/zio-http/src/main/scala/zio/http/QueryParams.scala @@ -21,6 +21,7 @@ import java.nio.charset.Charset import zio.Chunk import zio.http.Charsets +import zio.http.codec.TextCodec import zio.http.internal.QueryParamEncoding /** @@ -88,11 +89,22 @@ 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]): Option[Chunk[A]] = + map.get(key).map(_.flatMap(codec.decode)) + /** * 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]): Option[A] = get(key).flatMap(codec.decode) + /** * Retrieves all query parameter values having the specified name, or else * uses the default iterable. @@ -100,6 +112,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 +126,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/test/scala/zio/http/QueryParamsSpec.scala b/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala index 277ace21ed..17717f0d9e 100644 --- a/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala +++ b/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala @@ -232,14 +232,27 @@ object QueryParamsSpec extends ZIOHttpSpec { ), suite("get - getAll")( test("success") { - val name = "name" - 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) + val name = "name" + val typed = "typed" + val default = "default" + val typedDefault = 3 + val unknown = "non-existent" + val queryParams = QueryParams(name -> "a", name -> "b", typed -> "1", typed -> "2") + println(queryParams.getAsOrElse[Int](typed, typedDefault) == typedDefault) + 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.getAllOrElse(unknown, Chunk(default)).length == 1, + queryParams.getAs[Int](typed).get == 1, + queryParams.getAs[Int](unknown).isEmpty, + queryParams.getAsOrElse[Int](typed, typedDefault) == 1, + queryParams.getAsOrElse[Int](unknown, typedDefault) == typedDefault, + queryParams.getAllAs[Int](typed).get.length == 2, + queryParams.getAllAsOrElse[Int](unknown, Chunk(typedDefault)).length == 1, + ) }, ), suite("encode - decode")( From 36f75b94435f8e71ef13b7792061797e65908197 Mon Sep 17 00:00:00 2001 From: Maciej Bany Date: Thu, 2 Nov 2023 13:21:03 +0100 Subject: [PATCH 2/5] Added getAs for QueryParams - with typed errors --- .../src/main/scala/zio/http/QueryParams.scala | 22 +++++++--- .../scala/zio/http/QueryParamsError.scala | 44 +++++++++++++++++++ .../test/scala/zio/http/QueryParamsSpec.scala | 42 ++++++++++++------ 3 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 zio-http/src/main/scala/zio/http/QueryParamsError.scala diff --git a/zio-http/src/main/scala/zio/http/QueryParams.scala b/zio-http/src/main/scala/zio/http/QueryParams.scala index 1a9d63ac7c..500181ab5c 100644 --- a/zio-http/src/main/scala/zio/http/QueryParams.scala +++ b/zio-http/src/main/scala/zio/http/QueryParams.scala @@ -18,10 +18,10 @@ package zio.http import java.nio.charset.Charset -import zio.Chunk +import zio.{Chunk, NonEmptyChunk} import zio.http.Charsets -import zio.http.codec.TextCodec +import zio.http.codec.{HttpCodecError, TextCodec} import zio.http.internal.QueryParamEncoding /** @@ -92,8 +92,17 @@ 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]): Option[Chunk[A]] = - map.get(key).map(_.flatMap(codec.decode)) + 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.MalformedQueryParam(key, param, codec))) + .partitionMap(identity) match { + case (errors, _) if errors.nonEmpty => Left(QueryParamsError.MultiMalformedQueryParam(errors)) + case (_, typedParams) => Right(typedParams) + } + case None => Left(QueryParamsError.MissingQueryParam(key)) + } /** * Retrieves the first query parameter value having the specified name. @@ -103,7 +112,10 @@ 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]): Option[A] = get(key).flatMap(codec.decode) + def getAs[A](key: String)(implicit codec: TextCodec[A]): Either[QueryParamsError, A] = for { + param <- get(key).toRight(QueryParamsError.MissingQueryParam(key)) + typedParam <- codec.decode(param).toRight(QueryParamsError.MalformedQueryParam(key, param, codec)) + } yield typedParam /** * Retrieves all query parameter values having the specified name, or else 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..f873facce8 --- /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 MissingQueryParam(queryParamName: String) extends QueryParamsError { + def message = s"Missing query parameter with name $queryParamName" + } + + final case class MalformedQueryParam(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 MultiMalformedQueryParam(chunk: Chunk[MalformedQueryParam]) 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 17717f0d9e..a3d56adf84 100644 --- a/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala +++ b/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala @@ -232,26 +232,42 @@ object QueryParamsSpec extends ZIOHttpSpec { ), suite("get - getAll")( test("success") { - val name = "name" - val typed = "typed" - val default = "default" - val typedDefault = 3 - val unknown = "non-existent" - val queryParams = QueryParams(name -> "a", name -> "b", typed -> "1", typed -> "2") - println(queryParams.getAsOrElse[Int](typed, typedDefault) == typedDefault) + val name = "name" + val default = "default" + val unknown = "non-existent" + val queryParams = QueryParams(name -> "a", name -> "b") 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, - queryParams.getAs[Int](typed).get == 1, - queryParams.getAs[Int](unknown).isEmpty, - queryParams.getAsOrElse[Int](typed, typedDefault) == 1, - queryParams.getAsOrElse[Int](unknown, typedDefault) == typedDefault, - queryParams.getAllAs[Int](typed).get.length == 2, - queryParams.getAllAsOrElse[Int](unknown, Chunk(typedDefault)).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, ) }, ), From 9ae1bd00d28e446c1e619a962ea38a9b2e13b31d Mon Sep 17 00:00:00 2001 From: Maciej Bany Date: Thu, 2 Nov 2023 14:03:04 +0100 Subject: [PATCH 3/5] Added getAs for QueryParams - Errors naming fix --- zio-http/src/main/scala/zio/http/QueryParams.scala | 10 +++++----- .../src/main/scala/zio/http/QueryParamsError.scala | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/QueryParams.scala b/zio-http/src/main/scala/zio/http/QueryParams.scala index 500181ab5c..103ac40509 100644 --- a/zio-http/src/main/scala/zio/http/QueryParams.scala +++ b/zio-http/src/main/scala/zio/http/QueryParams.scala @@ -96,12 +96,12 @@ final case class QueryParams(map: Map[String, Chunk[String]]) { map.get(key) match { case Some(params) => params - .map(param => codec.decode(param).toRight(QueryParamsError.MalformedQueryParam(key, param, codec))) + .map(param => codec.decode(param).toRight(QueryParamsError.Malformed(key, param, codec))) .partitionMap(identity) match { - case (errors, _) if errors.nonEmpty => Left(QueryParamsError.MultiMalformedQueryParam(errors)) + case (errors, _) if errors.nonEmpty => Left(QueryParamsError.MultiMalformed(errors)) case (_, typedParams) => Right(typedParams) } - case None => Left(QueryParamsError.MissingQueryParam(key)) + case None => Left(QueryParamsError.Missing(key)) } /** @@ -113,8 +113,8 @@ 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 { - param <- get(key).toRight(QueryParamsError.MissingQueryParam(key)) - typedParam <- codec.decode(param).toRight(QueryParamsError.MalformedQueryParam(key, param, codec)) + param <- get(key).toRight(QueryParamsError.Missing(key)) + typedParam <- codec.decode(param).toRight(QueryParamsError.Malformed(key, param, codec)) } yield typedParam /** diff --git a/zio-http/src/main/scala/zio/http/QueryParamsError.scala b/zio-http/src/main/scala/zio/http/QueryParamsError.scala index f873facce8..4ff65ab9a7 100644 --- a/zio-http/src/main/scala/zio/http/QueryParamsError.scala +++ b/zio-http/src/main/scala/zio/http/QueryParamsError.scala @@ -30,15 +30,15 @@ sealed trait QueryParamsError extends Exception with NoStackTrace { def message: String } object QueryParamsError { - final case class MissingQueryParam(queryParamName: String) extends QueryParamsError { + final case class Missing(queryParamName: String) extends QueryParamsError { def message = s"Missing query parameter with name $queryParamName" } - final case class MalformedQueryParam(name: String, value: String, codec: TextCodec[_]) extends QueryParamsError { + 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 MultiMalformedQueryParam(chunk: Chunk[MalformedQueryParam]) extends QueryParamsError { + final case class MultiMalformed(chunk: Chunk[Malformed]) extends QueryParamsError { def message: String = chunk.map(_.getMessage()).mkString("; ") } } From 68be226fbc2abf3bec72c34e0f880f7938cdf410 Mon Sep 17 00:00:00 2001 From: Maciej Bany Date: Thu, 2 Nov 2023 23:07:37 +0100 Subject: [PATCH 4/5] Added getAs for QueryParams - imports fix --- zio-http/src/main/scala/zio/http/QueryParams.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/QueryParams.scala b/zio-http/src/main/scala/zio/http/QueryParams.scala index 103ac40509..ba01a4c4f5 100644 --- a/zio-http/src/main/scala/zio/http/QueryParams.scala +++ b/zio-http/src/main/scala/zio/http/QueryParams.scala @@ -18,10 +18,9 @@ package zio.http import java.nio.charset.Charset -import zio.{Chunk, NonEmptyChunk} +import zio.Chunk -import zio.http.Charsets -import zio.http.codec.{HttpCodecError, TextCodec} +import zio.http.codec.TextCodec import zio.http.internal.QueryParamEncoding /** From dbaaac95cb512d6f07c2465b5302049adb7c4be6 Mon Sep 17 00:00:00 2001 From: Maciej Bany Date: Fri, 3 Nov 2023 09:00:37 +0100 Subject: [PATCH 5/5] Added getAs for QueryParams - added getAsZIO --- .../src/main/scala/zio/http/QueryParams.scala | 15 ++++++++++++++- .../src/test/scala/zio/http/QueryParamsSpec.scala | 8 +++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/QueryParams.scala b/zio-http/src/main/scala/zio/http/QueryParams.scala index ba01a4c4f5..7913a40053 100644 --- a/zio-http/src/main/scala/zio/http/QueryParams.scala +++ b/zio-http/src/main/scala/zio/http/QueryParams.scala @@ -18,7 +18,7 @@ package zio.http import java.nio.charset.Charset -import zio.Chunk +import zio.{Chunk, IO, ZIO} import zio.http.codec.TextCodec import zio.http.internal.QueryParamEncoding @@ -103,6 +103,13 @@ final case class QueryParams(map: Map[String, Chunk[String]]) { 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. */ @@ -116,6 +123,12 @@ final case class QueryParams(map: Map[String, Chunk[String]]) { 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. diff --git a/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala b/zio-http/src/test/scala/zio/http/QueryParamsSpec.scala index a3d56adf84..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} @@ -269,6 +269,12 @@ object QueryParamsSpec extends ZIOHttpSpec { 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")(