How to make custom type binders for Play! Forms
If you've used play! then you know that it comes with a number of
form helpers that help define the types of data in a form, such as
nonEmptyText, boolean, email, and a number of others. These, as
far as the type goes, map to normal primitives like String, Long,
Int, and in the case of the date helpers, to Date, sql.Date,
and DateTime.
There are Mappings, and then there are Formats. They perform similar
methods within play: binding values to forms and form fields. So what's
the difference between the two? A Formatter is what's looked for when
one calls of[T] when setting the type of a form element. Like so:
import java.util.UUID val myForm = new Form[(String, UUID)]( tuple( "str" -> text, "uuid" -> of[UUID] ) )
of will look for an implicit Format for the type given to use when
trying to bind and unbind the field uuid. This is as simple as looking
at the trait documentation for Formatters and implementing it for the
type:
import play.api.data.FormError
import play.api.data.format.Formatter
implicit val UUIDFormat = new Formatter[UUID] {
def bind(key: String, data: Map[String, String]): Either[Seq[FormError], UUID] = {
data.get(key).map(UUID.fromString(_)).toRight(Seq(FormError(key, "forms.invalid.uuid", data.get(key).getOrElse(key))))
}
def unbind(key: String, value: UUID): Map[String, String] = Map(key -> value.toString)
}
The bind method is used to transform text data from the submitted form
into the required type. The result of the bind method is an Either,
with the failed left projection indicating a FormError has occured.
The arguments to the FormError are similar to the arguments to defining
a custom Constraint in play, the forms.invalid.uuid indicates what
message from the Message's API will be loaded if it's in scope, and the
arguments after the hard-coded string correspond to any number of arguments
that will be interpolated by the messages parameter substitution.*
The unbind method, unsurprisingly, does the opposte of the bind
statement in that we convert from our type to a string so that we can
pass the form field to any templates requiring us to.
*In a messages file, if you set something like, forms.invalid.uuid={0} is invalid, then you're going to see the first argument given to the FormError where that {0} is.
A Mapping had a bit more methods than just bind and unbind, however
they're very easily composable, allowing the creation of custom type
mappings be leveraging existing ones. For example, to create a UUID
Mapping we can leverage the existing text mapping:
def uuid: Mapping[UUID] = {
text.transform(UUID.fromString _, _.toString)
}
Though, this isn't as safe as it could be, we could be safer if we verified that
the text was a valid UUID first via a constraint:
val validUUID = Constraint[String]("forms.constraint.uuid") { str =>
Try(UUID.fromString(str)) match {
case Success(uuid) => Valid
case Failure(e) => Invalid(ValidationError("forms.invalid.uuid", str))
}
}
def uuid: Mapping[UUID] = {
text.verifying(validUUID).transform(UUID.fromString _, _.toString)
}
Once we have the mapping defined, we can use it in a form like:
val myForm = new Form[(String, UUID)]( tuple( ..., "uuid" -> uuid, ... ) )
And we'll get a useful error message if we can't bind the UUID for any reason.
All without implicits because we've explicitly provided a mapping. From my
understanding, this is the main difference between the two. As, calling of
will actually result in a FieldMapping being created.
So, which one should you prefer? Creating a Mapping, or a Formatter?
I think that this is a matter of preference and your concern over compilation
times. It's a given that when the compiler has to resolve an implicit type
during it's "proofing" of the code that this will take longer than if it
doesn't. So if you're working on a very large project, and compilation
time is an issue, I'd suggest favoring Mappings rather than using of.
I also find defining a Constraint and then using verifying to be more
understandable, just from a semantic point of view. After all, when I
think about binding values to and from a form, I don't think "format", I
think "mapping".