Skip to content

Commit

Permalink
Add getAs for QueryParams (#2505)
Browse files Browse the repository at this point in the history
* add getAs[A] for QueryParams

* Add getAs for QueryParams - with typed errors

* Add getAs for QueryParams - Errors naming fix

* Add getAs for QueryParams - imports fix

* Add getAs for QueryParams - added getAsZIO
  • Loading branch information
BanyMaciej authored Nov 6, 2023
1 parent 9909ee1 commit 40c2961
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 7 deletions.
54 changes: 52 additions & 2 deletions zio-http/src/main/scala/zio/http/QueryParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -88,25 +88,75 @@ 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.
*/
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.
*/
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

/**
Expand Down
44 changes: 44 additions & 0 deletions zio-http/src/main/scala/zio/http/QueryParamsError.scala
Original file line number Diff line number Diff line change
@@ -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("; ")
}
}
45 changes: 40 additions & 5 deletions zio-http/src/test/scala/zio/http/QueryParamsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down Expand Up @@ -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")(
Expand Down

0 comments on commit 40c2961

Please sign in to comment.