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)?
hochgi
October 25, 2023, 2:51am
2
This is a limitation in scala (2, in scala 3 it could’ve worked , but tapir is cross built to scala 2 as well)
As a general rule of thumb, you probably want to avoid classes and function with high arity
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
adamw
October 31, 2023, 3:15pm
3
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
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(_))
}
}