I want to decode an optional query parameter in my Scala code. I'm using http4s. The parameter is of the form ?part=35/43
. End goal is to store this fraction as a Type Part = (Int, Int)
so that we can have (35, 43)
as a tuple to use further in the code. I've created an Object like:
https://http4s.org/v0.18/dsl/#optional-query-parameters
object OptionalPartitionQueryParamMatcher
extends OptionalValidatingQueryParamDecoderMatcher[Part]("part")
Now OptionalValidatingQueryParamDecoderMatcher
needs an implicit QueryParamDecoder[Part]
in scope.
For this I created an implicit val, which needs to check if we actually have a valid fraction, which is to say, both the chars should be a digit (and not a/1
or b/c
etc) and the fraction should be less than 1 (1/2
, 5/8
etc):
implicit val ev: QueryParamDecoder[Part] =
new QueryParamDecoder[Part] {
def decode(
partition: QueryParameterValue
): ValidatedNel[ParseFailure, Part] = {
val partAndPartitions = partition.value.split("/")
Validated
.catchOnly[NumberFormatException] {
val part = partAndPartitions(0).toInt
val partitions = partAndPartitions(1).toInt
if (
partAndPartitions.length != 2 || part > partitions || part <= 0 || partitions <= 0
) {
throw new IllegalArgumentException
}
(part, partitions)
}
.leftMap(e => ParseFailure("Invalid query parameter part", e.getMessage))
.toValidatedNel
}
}
The problem with above code is, it only catches NumberFormatException
(that too when it can't convert a string to Int using .toInt
) but what if I input something like ?part=/1
, it should then catch ArrayIndexOutOfBoundsException
because I'm querying the first two values in the Array, or let's say IllegalArgumentException
when the fraction is not valid at all. How can I achieve that, catching everything in a single pass? Thanks!
well, the simplest approach would be to use .catchOnly[Throwable]
(or even QueryParamDecoder .fromUnsafeCast
directly) and it will catch any error.
However, I personally would prefer to do something like this:
(I couldn't compile the code right now, so apologies if it has some typos)
implicit final val PartQueryParamDecoder: QueryParamDecoder[Part] =
QueryParamDecoder[String].emap { str =>
def failure(details: String): Either[ParseFailure, Part] =
Left(ParseFailure(
sanitized = "Invalid query parameter part",
details = s"'${str}' is not properly formttated: ${details}"
))
str.split('/').toList match {
case aRaw :: bRaw :: Nil =>
(aRaw.toIntOption, bRaw.toIntOption) match {
case (Some(a), Some(b)) =>
Right(Part(a, b))
case _ =>
failure(details = "Some of the fraction parts are not numbers")
}
case _ =>
failure(details = "It doesn't correspond to a fraction 'a/b'")
}
}