scalaquery-stringscala-catshttp4s

Decode an optional query parameter using QueryParamDecoder in Scala


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!


Solution

  • 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'")
        }
      }