Case insensitive parameter parsing in Spray routing

485 views Asked by At

I would like to make parameter parsing in Spray routing case insensitive. For example:

val route: Route = {
  (path("search") & get) {
    parameters('pagesize.as[Int] ?, 'appId ?) { (pageSize, appId) => 
      ...
    }
  }
}

In this route, I would like the pageSize and appId parameters to work as case insensitive. For example, pagesize=5 OR PAGESIZE=5.

2

There are 2 answers

0
Mustafa Simav On

Looks like getting query parameters from URI logic is hardcoded in ParamDefMagnet2.filter function.

In order to overcome this limitation, I would duplicate that code, replace logic with ctx.request.uri.query.find(_._1.equalsIgnoreCase(paramName)) and import it when needed.

Example usage would look like this:

import CaseInsensitiveQueryParameters._

val route: Route = {
  (path("search") & get) {
    parameters('pagesize.as[Int] ?, 'appId ?) { (pageSize, appId) => 
      ...
    }
  }
}

And changed implicits would look like this:

object CaseInsensitiveQueryParameters {
  type ParamDefMagnetAux[A, B] = ParamDefMagnet2[A] { type Out = B }
  def ParamDefMagnetAux[A, B](f: A ⇒ B) = new ParamDefMagnet2[A] { type Out = B; def apply(value: A) = f(value) }

  import spray.httpx.unmarshalling.{ FromStringOptionDeserializer ⇒ FSOD, _ }
  import BasicDirectives._
  import RouteDirectives._

  /************ "regular" parameter extraction ******************/

  private def extractParameter[A, B](f: A ⇒ Directive1[B]) = ParamDefMagnetAux[A, Directive1[B]](f)
  private def filter[T](paramName: String, fsod: FSOD[T]): Directive1[T] =
    extract(ctx ⇒ fsod(ctx.request.uri.query.find(_._1.equalsIgnoreCase(paramName)))).flatMap {
      case Right(x)                             ⇒ provide(x)
      case Left(ContentExpected)                ⇒ reject(MissingQueryParamRejection(paramName))
      case Left(MalformedContent(error, cause)) ⇒ reject(MalformedQueryParamRejection(paramName, error, cause))
      case Left(x: UnsupportedContentType)      ⇒ throw new IllegalStateException(x.toString)
    }
  implicit def forString(implicit fsod: FSOD[String]) = extractParameter[String, String] { string ⇒
    filter(string, fsod)
  }
  implicit def forSymbol(implicit fsod: FSOD[String]) = extractParameter[Symbol, String] { symbol ⇒
    filter(symbol.name, fsod)
  }
  implicit def forNDesR[T] = extractParameter[NameDeserializerReceptacle[T], T] { nr ⇒
    filter(nr.name, nr.deserializer)
  }
  implicit def forNDefR[T](implicit fsod: FSOD[T]) = extractParameter[NameDefaultReceptacle[T], T] { nr ⇒
    filter(nr.name, fsod.withDefaultValue(nr.default))
  }
  implicit def forNDesDefR[T] = extractParameter[NameDeserializerDefaultReceptacle[T], T] { nr ⇒
    filter(nr.name, nr.deserializer.withDefaultValue(nr.default))
  }
  implicit def forNR[T](implicit fsod: FSOD[T]) = extractParameter[NameReceptacle[T], T] { nr ⇒
    filter(nr.name, fsod)
  }
}
0
vitalii On

I took Mustafa's answer one step further and extended parameter syntax, so you can use non-case sensitive parameters together with case-sensitive (Big Hooray for typeclasses and magnet pattern!):

package caseinsensitive

import spray.routing.SimpleRoutingApp
import akka.actor.ActorSystem
import spray.routing.directives.BasicDirectives
import spray.routing.directives.RouteDirectives
import scala.language.implicitConversions  

/**/

import spray.httpx.unmarshalling.{ FromStringOptionDeserializer ⇒ FSOD, _}

trait ToCINameReceptaclePimps {
  implicit def symbol2CINR(symbol: Symbol) = new CINameReceptacleHelper[String](symbol.name)
  implicit def string2CINR(string: String) = new CINameReceptacleHelper[String](string)
}


case class CINameReceptacleHelper[T](name: String) {
  def insensitive = CINameReceptacle[T](name)
}

case class CINameReceptacle[A](name: String) {
  def as[B] = CINameReceptacle[B](name)
  def as[B](deserializer: FSOD[B]) = CINameDeserializerReceptacle(name, deserializer)
  def ? = as[Option[A]]
  def ?[B](default: B) =CINameDefaultReceptacle(name, default)
  def ![B](requiredValue: B) = CIRequiredValueReceptacle(name, requiredValue)
}

case class CINameDeserializerReceptacle[A](name: String, deserializer: FSOD[A]) {
  def ? = CINameDeserializerReceptacle(name, Deserializer.liftToTargetOption(deserializer))
  def ?(default: A) = CINameDeserializerDefaultReceptacle(name, deserializer, default)
  def !(requiredValue: A) = CIRequiredValueDeserializerReceptacle(name, deserializer, requiredValue)
}

case class CINameDefaultReceptacle[A](name: String, default: A)

case class CIRequiredValueReceptacle[A](name: String, requiredValue: A)

case class CINameDeserializerDefaultReceptacle[A](name: String, deserializer: FSOD[A], default: A)

case class CIRequiredValueDeserializerReceptacle[A](name: String, deserializer: FSOD[A], requiredValue: A)



/**/

trait CaseInsensitiveParams extends ToCINameReceptaclePimps {

  import spray.routing._
  import spray.routing.directives._
  import spray.httpx.unmarshalling.{ FromStringOptionDeserializer ⇒ FSOD, _ }
  import BasicDirectives._
  import RouteDirectives._




  type ParamDefMagnetAux[A, B] = ParamDefMagnet2[A] { type Out = B }
  def ParamDefMagnetAux[A, B](f: A ⇒ B) = new ParamDefMagnet2[A] { type Out = B; def apply(value: A) = f(value) }

  private def extractParameter[A, B](f: A ⇒ Directive1[B]) = ParamDefMagnetAux[A, Directive1[B]](f)

   private def filterCI[T](paramName: String, fsod: FSOD[T]): Directive1[T] =
    extract(ctx ⇒ fsod(ctx.request.uri.query.find(_._1.equalsIgnoreCase(paramName)).map(_._2))).flatMap {
      case Right(x)                             ⇒ provide(x)
      case Left(ContentExpected)                ⇒ reject(MissingQueryParamRejection(paramName))
      case Left(MalformedContent(error, cause)) ⇒ reject(MalformedQueryParamRejection(paramName, error, cause))
      case Left(x: UnsupportedContentType)      ⇒ throw new IllegalStateException(x.toString)
    }


  /************ "regular" parameter extraction ******************/

  implicit def forCINDesR[T] = extractParameter[CINameDeserializerReceptacle[T], T] { nr ⇒
    filterCI(nr.name, nr.deserializer)
  }
  implicit def forCINDefR[T](implicit fsod: FSOD[T]) = extractParameter[CINameDefaultReceptacle[T], T] { nr ⇒
    filterCI(nr.name, fsod.withDefaultValue(nr.default))
  }
  implicit def forCINDesDefR[T] = extractParameter[CINameDeserializerDefaultReceptacle[T], T] { nr ⇒
    filterCI(nr.name, nr.deserializer.withDefaultValue(nr.default))
  }
  implicit def forCINR[T](implicit fsod: FSOD[T]) = extractParameter[CINameReceptacle[T], T] { nr ⇒
    filterCI(nr.name, fsod)
  }

  /************ required parameter support ******************/

  private def requiredFilterCI(paramName: String, fsod: FSOD[_], requiredValue: Any): Directive0 =
    extract(ctx ⇒ fsod(ctx.request.uri.query.find(_._1.equalsIgnoreCase(paramName)).map(_._2))).flatMap {
      case Right(value) if value == requiredValue ⇒ pass
      case _                                      ⇒ reject
    }

  implicit def forCIRVR[T](implicit fsod: FSOD[T]) = ParamDefMagnetAux[CIRequiredValueReceptacle[T], Directive0] { rvr ⇒
    requiredFilterCI(rvr.name, fsod, rvr.requiredValue)
  }
  implicit def forCIRVDR[T] = ParamDefMagnetAux[CIRequiredValueDeserializerReceptacle[T], Directive0] { rvr ⇒
    requiredFilterCI(rvr.name, rvr.deserializer, rvr.requiredValue)
  }




}




object Main extends App with SimpleRoutingApp  with CaseInsensitiveParams {
  implicit val system = ActorSystem("my-system")

  startServer(interface = "localhost", port = 8080) {
    path("hello") {
      parameters("foo".insensitive.?) { foo =>
        get {
          complete {
            <h1>You said {foo} </h1>
          }
        }
      }
    }
  }
}

If you don't need all of this than you can just use parameterMap directive and get parameters from there.