Map 22+ query params to case class

Hi, I have endpoint with 23+ query params, and of sake of convenience I`ve mapped them to case class.

    query[Option[String]]("q1")
      .and(query[Option[String]]("q2"))
      .and(query[Option[String]]("q3"))
      //....
    .and(query[Option[String]]("q22"))
    .mapTo[EndpointParams]

and it works just fine untill 23rd parameter in list. The error is following:

The arity of the source type doesn't match the arity of the target type

So my question is how to handle such cases(23+ query params mapped to case class)?

This is a limitation in scala (2, in scala 3 it could’ve worked, but tapir is cross built to scala 2 as well)

  1. As a general rule of thumb, you probably want to avoid classes and function with high arity
  2. You can group subsets of parameters, and compose, e.g. something like: p1.and(p2).mapTo[CaseCls1].and(p3.and(p4).mapTo[CaseCls2]).mapTo[CaseCls3]
2 Likes

Yes, we have tuple operations defined only up to 22: https://github.com/softwaremill/tapir/blob/462ca9dfa4ee78c7e8dd0c3422ef779f1e44968c/core/src/main/boilerplate-gen/sttp/tapir/typelevel/TupleOps.scala#L724

As @hochgi mentioned, we could fix this using Scala 3, but so far this code is kept the same for Scala 2/ Scala 3. Not that we wouldn’t welcome a PR changing this, but so far nobody complained :slight_smile:

2 Likes
trait FilterInput[F] {
  def input(filter: F): EndpointInput[F]
}

object FilterInput {

  def input[F](filter: F)(implicit input: FilterInput[F]): EndpointInput[F] = input.input(filter)

  implicit def filterInstance[T <: FilterType](implicit w: Witness.Aux[T]): FilterInput[Filter[T]] =
    _ =>
      w.value.input
        .asInstanceOf[EndpointInput[Option[T#Content]]] // scalafix:ok
        .map(Filter[T](_))(_.value)

  implicit def lastInstance[H](implicit lastInput: FilterInput[H]): FilterInput[H :: HNil] =
    (last: H :: HNil) => lastInput.input(last.head).map(_ :: HNil)(_.head)

  implicit def hListInstance[H, L <: HList](implicit
    headInput: FilterInput[H],
    tailInput: FilterInput[L]
  ): FilterInput[H :: L] = { case head :: tail =>
    (headInput.input(head) and tailInput.input(tail))
      .map(t => t._1 :: t._2) { case head :: tail => head -> tail }
  }
}

object SomewhereElse {
  final case class Filter[T <: FilterType](value: Option[T#Content]) extends AnyVal

  object Filter {
    def empty[T <: FilterType]: Filter[T] = Filter(None)
  }

  sealed trait FilterType extends EnumEntry {
    type Content
    def input: EndpointInput[Option[Content]]
    // other methods, for instance def filter(content: Content): Cat => Boolean
  }

  object FilterType extends Enum[FilterType] {
    case object Filter1 extends FilterType {
      override type Content = String
      override def input: EndpointInput[Option[String]] = query[Option[String]]("q1")
    }
    case object Filter2 extends FilterType {
      override type Content = String
      override def input: EndpointInput[Option[String]] = query[Option[String]]("q2")
    }
    override def values: IndexedSeq[FilterType] = findValues
  }

  final case class Params(
    filter1: Filter[Filter1.type] = Filter.empty,
    filter2: Filter[Filter2.type] = Filter.empty
  )

  object Params {
    private val Empty = Params()

    val Input: EndpointInput[Params] =
      FilterInput
        .input(Generic[Params].to(Empty))
        .map(Generic[Params].from(_))(Generic[Params].to(_))
  }
}