I have observed that when I add more than 18 parameters to a Play Framework Form-class I get a long (and for me incomprehensible) compilation error.
Is this a documented limitation? I need to take in as much as 29 parameters in a form post. I don't decide on the design and number of parameters as I am implementing a protocol from an open standard.
I'm mapping like this:
val registration = Form(mapping(
"client_type" -> nonEmptyText,
"client_id" -> optional(nonEmptyText),
... up to 29 args, all optional(nonEmptyText)
){ (clientType, clientId ...) => RegistrationRequest(clientType, clientId ...) }
{ req => None })
My strategy was to do the mapping this way instead of apply/unapply and create an heirarchy of case classes. The reason is to work around the 22 arguments limit in Case classes, which was the first seemingly arbitrary limit I ran into. Up to 18 args mapping works, after that I get a long compilation error.
The error message can be found here (too long to include): https://gist.github.com/2928297
I'm looking for suggestions on how I can get around this limitation. I know it is bad design to send in 29 parameters in a form Post, but it should still be possible.
Hack/Workaround/Solution
Ok, here is my hacked together workaround (writing this post took much longer than implementing, I hacked for ~30min on this)
I wrote functions that preprocesses the request params and adds a group prefix to group certain params. I then use the resulting Map[String, String] and continue processing with the form class, doing validation etc as usual. This allows me to use nested case classes in the mapping and get below the 18 params limit.
Beware: ugly code ahead! I should probably not show early hacky code like this, but I'm hoping it will help someone else who wants a workaround.
def preprocessFormParams(prefix:String, replace:String)(implicit request:Request[AnyContent]):Map[String, String] = request.body.asFormUrlEncoded.map( _.filterKeys( _.startsWith(prefix)).map( m => m._1.patch(0, replace, prefix.length) -> m._2.head )).getOrElse(Map.empty)
def unprocessedFormParams(prefixes:Set[String])(implicit request:Request[AnyContent]):Map[String, String] = request.body.asFormUrlEncoded.map( _.filterKeys( !prefixes.contains(_) ).map( m => m._1 -> m._2.head )).getOrElse(Map.empty)
So these functions should probably be for comprehensions or split up, but here goes: preprocessedFormParms takes a prefix and replaces it:
val clientParams = preprocessFormParams("client_", "client.")
("client_id" -> "val1", "client_type" -> "val2") becomes ("client.id" -> "val1", "client.type" -> "val2")
When I have the parameters in the form of group.key1, group.key2 I can nest the case classes in the form like so
Form(mapping("client" -> mapping("type" -> nonEmptyText
"id" -> optional(nonEmptyText),
"secret" -> optional(nonEmptyText))
(RegisterClient.apply)(RegisterClient.unapply)
... more params ...)
(RegisterRequest.apply)(RegisterRequest.unapply)
In my action I go ahead and filter out each of my groups
implicit request =>
val clientParams = preprocessFormParams("client_", "client.")
val applicationParams = preprocessFormParams("application_", "application.")
val unprocessedParams = unprocessedFormParams(Set("client_", "application_"))
val processedForm = clientParams ++ applicationParams ++ unprocessedParams
Lastly I can apply my form like normal but now I get the nested structure I that reduces the number of arguments and hopefully makes the case class more manageable.
clientRegistrationForm.bind(processedForm).fold( ... )
Using this approach you can keep the number of parameters down. If your parameters don't have the same prefix for easy grouping like my problem, then you can still use the same basic approach but filter on other criterias.