Last active
August 30, 2016 08:53
-
-
Save jeroenr/8956587 to your computer and use it in GitHub Desktop.
Parameter validation with Play 2 framework
This file contains hidden or 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
object ApplicationBuild extends Build { | |
val appName = "my-app" | |
val appVersion = "1-SNAPSHOT" | |
val appDependencies = Seq( | |
jdbc, | |
cache | |
) | |
val main = play.Project(appName, appVersion, appDependencies) | |
.settings( | |
routesImport += "binders._", | |
) | |
} |
This file contains hidden or 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
package binders | |
import play.api.mvc.QueryStringBindable | |
import util.{Try, Success, Failure} | |
import services.ParamValidator | |
import play.api.data.validation._ | |
import play.api.i18n.Messages | |
case class Pager(offset: Int, size: Int) | |
object Pager { | |
val NUM = "num" | |
val SIZE = "size" | |
val DEFAULT_NUM = 1 | |
val DEFAULT_SIZE = 30 | |
val CONSTRAINTS = Seq(ParamValidator.MIN_0, Constraints.max(50000000)) | |
implicit def queryStringBinder(implicit intBinder: QueryStringBindable[Int]) = new QueryStringBindable[Pager] { | |
override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, Pager]] = { | |
val pagingKeys = Seq(s"$key.$NUM", s"$key.$SIZE") | |
val pagingParams = pagingKeys.filter(params.keys.toSeq.contains(_)) | |
val result = for { | |
num <- Try(intBinder.bind(pagingKeys(0), params).get).recover { | |
case e => Right(DEFAULT_NUM) | |
} | |
size <- Try(intBinder.bind(pagingKeys(1), params).get).recover { | |
case e => Right(DEFAULT_SIZE) | |
} | |
} yield { | |
(num.right.toOption, size.right.toOption) | |
} | |
result match { | |
case Success((maybeNum, maybeSize)) => | |
ParamValidator(CONSTRAINTS, maybeNum, maybeSize) match { | |
case Valid => | |
Some(Right(Pager(maybeNum.get - 1, maybeSize.get))) | |
case Invalid(errors) => | |
Some(Left(errors.zip(pagingParams).map { | |
case (ValidationError(message, v), param) => Messages(message, param, v) | |
}.mkString(", "))) | |
} | |
case Failure(e) => Some(Left(s"Invalid paging params: ${e.getMessage}")) | |
} | |
} | |
override def unbind(key: String, pager: Pager): String = { | |
intBinder.unbind(s"$key.$NUM", pager.offset + 1) + "&" + intBinder.unbind(s"$key.$SIZE", pager.size) | |
} | |
} | |
} |
This file contains hidden or 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
package services | |
import play.api.data.validation.{Constraint, Invalid, Valid, Constraints} | |
/** | |
* Created by jeroen on 2/7/14. | |
*/ | |
object ParamValidator { | |
val MIN_0 = Constraints.min(0) | |
def apply[T](constraints: Iterable[Constraint[T]], optionalParam: Option[T]*) = | |
optionalParam.flatMap { _.map { param => | |
constraints flatMap { | |
_(param) match { | |
case i:Invalid => Some(i) | |
case _ => None | |
} | |
} | |
} | |
}.flatten match { | |
case Nil => Valid | |
case invalids => invalids.reduceLeft { | |
(a,b) => a ++ b | |
} | |
} | |
} |
This file contains hidden or 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
GET /api/users controllers.UserController.list(page: Pager) |
Great work! I like your solution, because it uses less match statements. I get type erasure warnings when I try to match the Seq[Invalid]
I especially like how you use flatten on the Seq[Option[ValidationResult]] to only keep the Invalid results. Then you don't need to partition :)
Btw, it's also easy to make it work for other parameter types
object ParamValidator {
def apply[T](constraints: Iterable[Constraint[T]], optionalParam: Option[T]*) =
optionalParam.flatMap { _.map { param =>
constraints flatMap {
_(param) match {
case i:Invalid => Some(i)
case _ => None
}
}
}
}.flatten match {
case Nil => Valid
case invalids => invalids.reduceLeft {
(a,b) => a ++ b
}
}
}
Hahaha I was looking back at the code right now and I had the same idea!
Haha great :). I made a pull request for this stuff here playframework/playframework#2377. Would be nice if they merge it in in some way.
Added sample of using the validator in a custom query string binder
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey Jeroen, nice idea! I wanted to understand how it worked and before I knew it I rewrote it with
flatMap
, see if you like it! :)Here's a small test I used to verify it behaves as intended: