Last active
August 29, 2015 14:07
-
-
Save mauhiz/fa8c79d6399c5f498b86 to your computer and use it in GitHub Desktop.
Unsafe form mappings for play forms which have > 22 fields
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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