2

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!

fan
  • 23
  • 4

1 Answers1

3

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'")
    }
  }
  • Works like a charm! Thank you so much! Been trying to work this out for 2 days straight. I was looking at the documentation of `emap` and couldn't get a clear picture of when to use `decode` vs `emap`. Would be great if you can shed some light on that. :) – fan May 26 '21 at 18:39
  • @fan `decode` is the **method** that the framework calls, and is the only method you need to implement if you are writing a `Decorder` from scratch. - `emap` _(and friends)_ are helpers to construct new `Decoders` from other `Decoders`, so as you can see what I did was assuming that the record could already be decoded as a `String` and then attempt to transform that `String` into a `Part` – Luis Miguel Mejía Suárez May 26 '21 at 19:24