scalaplayframework-2.3

Play cannot find custom QueryString binder in separate package


Trying to implement my first QueryStringBindable for models.DeviceContext:

case class DeviceContext( deviceIdLike: String = "", deviceUseridLike: String = "") 

I have util.Binders.scala as follows:

package util

import play.api.mvc.QueryStringBindable
import models._

object Binders {

  implicit def deviceContextBinder(implicit stringBinder: QueryStringBindable[String]) =
    new QueryStringBindable[DeviceContext] {

      override def bind(key: String, params: java.util.Map[String, Array[String]]): Option[Either[String, DeviceContext]] = {
        Some({
          val deviceIdLike = stringBinder.bind(key + ".deviceIdLike", params)
          val deviceIUseridLike = stringBinder.bind(key + ".deviceUseridLike", params)
          (deviceIdLike.isDefined && deviceIUseridLike.isDefined) match {
            case true => Right(DeviceContext(deviceIdLike.get, deviceIUseridLike.get))
            case false => Left("Unable to bind DeviceContext")
          }
        })
      }

      override def unbind(key: String, deviceContext: DeviceContext): String =
        stringBinder.unbind(
          key + ".deviceIdLike=" + deviceContext.deviceIdLike +
            "&" + key + ".deviceUserIdLike=" + deviceContext.deviceUseridLike)
    }

}

which is adapted from the example in the documentation. My build.sbt file attempts to make util.Binders visible to the routes, with:

import play.PlayImport.PlayKeys._
routesImport += "util.Binders._"

and the routes are things that look like:

GET  /device controllers.Devices.list(dc:models.DeviceContext)

However, the compiler is not happy:

[error] \conf\routes:76: No QueryString binder found for type models.DeviceContext. Try to implement an implicit QueryStringBindable for this type. [error] GET /device controllers.Devices.list(deviceContext:DeviceContext)

[error] \conf\routes:76: not enough arguments for method implicitly: (implicit e: play.api.mvc.QueryStringBindable[models.DeviceContext])play.api.mvc.QueryStringBindable[models.DeviceContext]. [error] Unspecified value parameter e.

Scala version is 2.11.5, play is 2.3.9

The import util.Binders is an explicit import, so should be included in the implicit search. It is as if the signature of implicit def deviceContextBinder does not match what is required, but it returns a QueryStringBindable of my type, so I can't see why it would not match.

Any pointers appreciated!

(On a side note, the documentation uses params: Map[String,Seq[String]] but if I use that, the compiler complains that it was expecting a java.util.Map[String,Array[String]] which seems odd... see answer below.)

EDIT 1: When I use standard data types, so as not to invoke my customer QueryStringBindable, but leave the code in the project, the compiler then reports new errors, the first of which is:

type arguments [String] do not conform to trait QueryStringBindable's type parameter bounds [T <: play.mvc.QueryStringBindable[T]]


Solution

  • Well, the solution was that I seemed to have to place the code in the same package as the case class. When I did that, the java.util.Map "workaround" mentioned above resolved, and so, dropping Binders.scala and extending DeviceContext.Scala to the following worked:

    package models
    
    case class DeviceContext(deviceIdLike: String = "", deviceUseridLike: String = "")
    
    object DeviceContext {
      import play.api.mvc.QueryStringBindable
    
      implicit def deviceContextBinder(implicit stringBinder: QueryStringBindable[String]) =
        new QueryStringBindable[DeviceContext] {
    
          override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, DeviceContext]] = {
            Some({
              val deviceIdLike = stringBinder.bind(key + ".deviceIdLike", params)
              val deviceIUseridLike = stringBinder.bind(key + ".deviceUseridLike", params)
              (deviceIdLike, deviceIUseridLike) match {
                case (Some(Right(di)), Some(Right(du))) => Right(DeviceContext(di, du))
                case _ => Left("Unable to bind DeviceContext")
              }
            })
          }
    
          override def unbind(key: String, deviceContext: DeviceContext): String =
            stringBinder.unbind(
              key + ".deviceIdLike", deviceContext.deviceIdLike) +
              "&" + stringBinder.unbind(key + ".deviceUserIdLike", deviceContext.deviceUseridLike)
        }
    
    }
    

    I also found that routesImports was only required if I wanted to refer to my type in an unqualified way in the routes file, i.e. controllers.Devices.list(dc:DeviceContext). If I am willing to write the parameter as dc:models.DeviceContext, then the routesImports was not required.