app/utils/filter.scala (88 lines of code) (raw):
package utils
import scala.util.matching.Regex
import play.api.libs.json._
import play.api.mvc.RequestHeader
trait Matchable[T] {
def isMatch(value: T): Boolean
}
case class StringMatchable(matcher: String) extends Matchable[String] {
def isMatch(value: String): Boolean = value == matcher
}
case class RegExMatchable(matcher: Regex) extends Matchable[String] {
def isMatch(value: String): Boolean = matcher.unapplySeq(value).isDefined
}
case class InverseMatchable[T](matcher: Matchable[T]) extends Matchable[T] {
def isMatch(value: T): Boolean = !matcher.isMatch(value)
}
case class ResourceFilter(filter: Map[String, Seq[Matchable[String]]])
extends Matchable[JsValue]
with Logging {
def isMatch(json: JsValue): Boolean = {
filter.map { case (field, values) =>
values -> field.split('.').foldLeft(json) { case (jv, part) =>
(jv \ part).getOrElse(JsNull)
}
} forall {
case (values, JsString(str)) => values exists (_.isMatch(str))
case (values, JsNumber(int)) => values exists (_.isMatch(int.toString()))
case (values, JsArray(seq)) =>
seq.exists {
case JsString(str) => values exists (_.isMatch(str))
case _ => false
}
case _ => false
}
}
def isMatch(map: Map[String, String]): Boolean = {
filter.map { case (field, values) =>
val value = map.get(field)
value match {
case None => true // no constraint? then match
case Some(string) => values exists (_.isMatch(string))
}
} forall (ok => ok)
}
}
object ResourceFilter {
// list of request keys that should be ignored
val DenyList = Set("callback")
val InverseRegexMatch: Regex = """^([a-zA-Z0-9.]*)(?:!~|~!)$""".r
val InverseMatch: Regex = """^([a-zA-Z0-9.]*)!$""".r
val RegexMatch: Regex = """^([a-zA-Z0-9.]*)~$""".r
val SimpleMatch: Regex = """^([a-zA-Z0-9.]*)$""".r
def matcher(
key: String,
value: String
): Option[(String, Matchable[String])] = {
key match {
case InverseRegexMatch(bareKey) =>
Some(bareKey -> InverseMatchable(RegExMatchable(value.r)))
case RegexMatch(bareKey) => Some(bareKey -> RegExMatchable(value.r))
case InverseMatch(bareKey) =>
Some(bareKey -> InverseMatchable(StringMatchable(value)))
case SimpleMatch(bareKey) => Some(bareKey -> StringMatchable(value))
case _ => None
}
}
def fromRequest(implicit request: RequestHeader): ResourceFilter =
fromRequestWithDefaults()
def fromRequestWithDefaults(
defaults: (String, String)*
)(implicit request: RequestHeader): ResourceFilter = {
val defaultKeys = defaults
.flatMap { d => matcher(d._1, d._2) }
.groupBy(_._1)
.view
.mapValues(_.map(_._2))
val filterKeys = request.queryString.view
.filterKeys(key => !DenyList.contains(key))
.toSeq
.flatMap { case (key, values) =>
values.flatMap(matcher(key, _))
}
.groupBy(_._1)
.view
.mapValues(_.map(_._2))
ResourceFilter((defaultKeys ++ filterKeys).toMap)
}
lazy val all: Matchable[JsValue] = (_: JsValue) => true
}