Skip to content

Instantly share code, notes, and snippets.

@mauhiz
Last active August 29, 2015 14:07
Show Gist options
  • Save mauhiz/fa8c79d6399c5f498b86 to your computer and use it in GitHub Desktop.
Save mauhiz/fa8c79d6399c5f498b86 to your computer and use it in GitHub Desktop.
Unsafe form mappings for play forms which have > 22 fields
import java.lang.reflect.Constructor
import play.api.data._
import play.api.data.validation.Constraint
import scala.reflect.ClassTag
import scala.util.Try
/**
* Workaround the annoying limit of 22 for argument lists. See https://github.com/mandubian/shapeless-rules for a better way to do it.
* Limitations:
* - no type checking field mappings at compile time
* - T must have a public constructor which matches the fields
*/
case class NotSoSafeMapping[T <: Product](fields: Seq[(String, Mapping[_])] = Nil, constraints: Seq[Constraint[T]] = Nil, key: String = "")(implicit classTag: ClassTag[T]) extends Mapping[T] with ObjectMapping {
private val mappedFields = fields.map {
case (fieldName, fieldMapping) => fieldMapping.withPrefix(fieldName).withPrefix(key)
}
private val apply = classTag.runtimeClass.getDeclaredConstructors.find(cons => isAcceptable(cons.asInstanceOf[Constructor[T]])).getOrElse{
throw new IllegalArgumentException(s"No ${mappedFields.size}-sized constructor found in type ${classTag.runtimeClass.getName}")
}
val mappings: Seq[Mapping[_]] = Seq(this) ++ mappedFields
private def isAcceptable(cons: Constructor[T]): Boolean = {
val paramTypes = cons.getParameterTypes
paramTypes.size == mappedFields.size
}
def verifying(addConstraints: Constraint[T]*): Mapping[T] = copy(constraints = constraints ++ addConstraints.toSeq)
def unbind(value: T): Map[String, String] = {
val fieldsUnbound = for {(fieldMapping, i) <- mappedFields.zipWithIndex} yield {
fieldMapping.unbind(value.productElement(i).asInstanceOf)
}
fieldsUnbound.foldLeft(Map.empty[String, String]) {
case (aggMap, itemMap) => aggMap ++ itemMap
}
}
def withPrefix(prefix: String): Mapping[T] = addPrefix(prefix) match {
case None => this
case Some(newKey) => copy(key = newKey)
}
def unbindAndValidate(value: T): (Map[String, String], Seq[FormError]) = {
if (value.productArity != mappedFields.size) {
throw new IllegalArgumentException(s"Value does not have the right number of fields (${mappedFields.size}): $value")
}
val fieldsUnboundAndValidated = for {(fieldMapping, i) <- mappedFields.zipWithIndex} yield {
fieldMapping.unbindAndValidate(value.productElement(i).asInstanceOf)
}
fieldsUnboundAndValidated.foldLeft((Map.empty[String, String], Seq.empty[FormError])) {
case ((aggMap, aggFormErrors), (itemMap, itemFormErrors)) => (aggMap ++ itemMap, aggFormErrors ++ itemFormErrors)
}
}
def bind(data: Map[String, String]): Either[Seq[FormError], T] = {
merge(mappedFields.map(_.bind(data)): _*) match {
case Left(errors) => Left(errors)
case Right(values) => {
if (fields.size == values.size) {
val args = new Array[Object](values.size)
values.zipWithIndex.foreach {case (v, i) => args(i) = v.asInstanceOf[Object]}
val t = Try { apply.newInstance(args: _*).asInstanceOf[T] }
if (t.isSuccess) applyConstraints(t.get)
else Left(Seq(FormError(key, "bind.failed")))
} else {
Left(Seq(FormError(key, "bind.failed")))
}
}
}
}
}
object NotSoSafeMapping {
def apply[T <: Product](fields: (String, Mapping[_])*)(implicit classTag: ClassTag[T]) = new NotSoSafeMapping[T](fields)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment