Autogenerating unnapply for play form handling in scala3 for single-element case classes

38 Views Asked by At

Scala 3 changed the signature of the auto-generated unapply method of case classes (see here). Unfortunately the unapply method is explicitly called when mapping forms for the play framework.

import play.api.data.Form
import play.api.data.Forms._

case class Thing(x: String, y: Int)

val f = Form(mapping(
  "x" -> nonEmptyText(),
  "y" -> number(),
)(Thing.apply)(Thing.unapply))

This snippet works in Scala2, but not in Scala3. The play documentation recommends to manually define a compatible unapply method, but i think we can do better.

And indeed, we can auto-generate the unapply method like this:

# Scala3 only
import scala.deriving.Mirror

object FormHelper:
    def unapply[A <: Product](value: A)(using mirror: Mirror.ProductOf[A]): Option[mirror.MirroredElemTypes] =
        Some(Tuple.fromProductTyped(value))

Defining forms now looks like this:

# Scala3 only

val f = Form(mapping(
  "x" -> nonEmptyText(),
  "y" -> number(),
)(Thing.apply)(FormHelper.unapply))

But now the problems begin. Given a case class with a single member:

case class Single(x: String)

My FormHelper.unapply returns an option of a tuple with a single element, like Option[Tuple1[T]]. But for some reasons the mapping function for play needs a flat Option[T].

So my question is: how can i extend my FormHelper (either with a new method, or extend the existing unapply) such that it returns Option[T] when given a case class with a single member.

The closest i could get to that was this

def single[A <: Product](value: A)(using mirror: Mirror.ProductOf[A]) = Some(Tuple.fromProductTyped(value).take(1))

The return type of that is Some[T *: EmptyTuple] and i'm not able to get rid of the EmptyTuple thing.

My guess is, that i somehow need to restrict the method to products of length 1, but unfortunately case classes only inherit from Product and not the specific ProductN[T...] traits.

Note: i know that i could use this to make the form definition work:

val f = Form(mapping(
  "x" -> nonEmptyText(),
)(Single.apply)(t => Some(t._1))

but i'm also interested how to do that with the meta-programming approach.

0

There are 0 best solutions below