3
votes

So I have a routes file that looks something like this:

GET     /myRes                 controllers.MyController.get(ids: Option[String], elems: Option[String])

All well and good. Users can get stuff by doing:

/myRes
/myRes?ids=X
/myRes?elems=Y
/myRes?ids=X&elems=Y

However, they can also query the interface by doing:

/myRes?id=X

Which is problematic, because in this case the user is gets the same result as if they had queried /myRes, which is almost certainly not the result they expected. This has been causing a lot of confusion/bugs for developers of the API. Is there an elegant way to catch incorrect/unspecified query parameters being passed to the controller and return a hard error for such queries?

Edit: Changed title to something more descriptive. My problem is basically validating the query parameters to catch any query parameters passed to the API which are not valid/correct.

3

3 Answers

0
votes

It can be done with the help of a macro annotation like the following one:

import scala.reflect.macros.whitebox.Context
import scala.language.experimental.macros
import scala.annotation.StaticAnnotation
import scala.annotation.compileTimeOnly
import play.api.mvc._

@compileTimeOnly("respond 400 bad request in case of unexpected params")
class StrictParams extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro StrictParamsMacro.impl
}

object StrictParamsMacro {
  def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._
    annottees.map(_.tree).toList match {
      case q"def $name(..$params) = $action { ..$body }" :: Nil =>
        val supportedParamNames = params.map(ab => ab.name.toString).toSet
        c.Expr[Any](
          q"""def $name(..$params) = { req: Request[_] =>
            val unsupportedParams = req.queryString.keySet -- $supportedParamNames
            if (unsupportedParams.nonEmpty) {
              BadRequest(unsupportedParams.mkString("Unsupported Params: ", ", ", ""))
            } else {
              $body
            }
          }"""
        )
    }
  }
}

Then you can annotate your action method like this:

@StrictParams
def get(ids: Option[String], elems: Option[String]) = Action {
  ...
}
0
votes

i usually pass it like this on get method

GET    /getSomething        Controllers.Application.getData()

GET    /getSomething/:id         Controllers.Application.getData(id:Integer)

GET    /getSomething/:id/:name          Controllers.Application.getData(id:Integer, name :String)
0
votes

You can define a QueryStringBindable[A] to bind a Map of query string parameters to an instance of type A. See the corresponding documentation. Didn't fully explore it yet, but it doesn't seem too difficult to implement a QueryStringBindable[Option[A]].

If you want to forbid confusing parameters (e.g. allowing ids but not id), you can check for unexpected keys in the parameters Map and return an error message if needed (although I would recommand to accept the id key to match the behaviour expected by users).