+htp extract multiple occurrences in the parameters/formFields directives by suffixing with .*

This commit is contained in:
Alek Storm 2015-03-30 10:33:46 -07:00
parent 632868b868
commit d3742c577a
9 changed files with 171 additions and 21 deletions

View file

@ -95,6 +95,50 @@ class ParameterDirectivesExamplesSpec extends RoutingSpec {
responseAs[String] shouldEqual "The query parameter 'count' was malformed:\n'blub' is not a valid 32-bit signed integer value" responseAs[String] shouldEqual "The query parameter 'count' was malformed:\n'blub' is not a valid 32-bit signed integer value"
} }
} }
"repeated" in {
val route =
parameters('color, 'city.*) { (color, cities) =>
cities.toList match {
case Nil => complete(s"The color is '$color' and there are no cities.")
case city :: Nil => complete(s"The color is '$color' and the city is $city.")
case multiple => complete(s"The color is '$color' and the cities are ${multiple.mkString(", ")}.")
}
}
Get("/?color=blue") ~> route ~> check {
responseAs[String] === "The color is 'blue' and there are no cities."
}
Get("/?color=blue&city=Chicago") ~> Route.seal(route) ~> check {
responseAs[String] === "The color is 'blue' and the city is Chicago."
}
Get("/?color=blue&city=Chicago&city=Boston") ~> Route.seal(route) ~> check {
responseAs[String] === "The color is 'blue' and the cities are Chicago, Boston."
}
}
"mapped-repeated" in {
val route =
parameters('color, 'distance.as[Int].*) { (color, cities) =>
cities.toList match {
case Nil => complete(s"The color is '$color' and there are no distances.")
case distance :: Nil => complete(s"The color is '$color' and the distance is $distance.")
case multiple => complete(s"The color is '$color' and the distances are ${multiple.mkString(", ")}.")
}
}
Get("/?color=blue") ~> route ~> check {
responseAs[String] === "The color is 'blue' and there are no distances."
}
Get("/?color=blue&distance=5") ~> Route.seal(route) ~> check {
responseAs[String] === "The color is 'blue' and the distance is 5."
}
Get("/?color=blue&distance=5&distance=14") ~> Route.seal(route) ~> check {
responseAs[String] === "The color is 'blue' and the distances are 5, 14."
}
}
"parameterMap" in { "parameterMap" in {
val route = val route =
parameterMap { params => parameterMap { params =>

View file

@ -25,7 +25,7 @@ Description
Form fields can be either extracted as a String or can be converted to another type. The parameter name Form fields can be either extracted as a String or can be converted to another type. The parameter name
can be supplied either as a String or as a Symbol. Form field extraction can be modified to mark a field can be supplied either as a String or as a Symbol. Form field extraction can be modified to mark a field
as required or optional or to filter requests where a form field has a certain value: as required, optional, or repeated, or to filter requests where a form field has a certain value:
``"color"`` ``"color"``
extract value of field "color" as ``String`` extract value of field "color" as ``String``
@ -40,6 +40,13 @@ as required or optional or to filter requests where a form field has a certain v
(see also :ref:`http-unmarshalling-scala`) (see also :ref:`http-unmarshalling-scala`)
``"amount".as(deserializer)`` ``"amount".as(deserializer)``
extract value of field "amount" with an explicit ``Deserializer`` extract value of field "amount" with an explicit ``Deserializer``
``"distance".*``
extract multiple occurrences of field "distance" as ``Iterable[String]``
``"distance".as[Int].*``
extract multiple occurrences of field "distance" as ``Iterable[Int]``, you need a matching implicit ``Deserializer`` in scope for that to work
(see also :ref:`unmarshalling`)
``"distance".as(deserializer).*``
extract multiple occurrences of field "distance" with an explicit ``Deserializer``
You can use :ref:`Case Class Extraction` to group several extracted values together into a case-class You can use :ref:`Case Class Extraction` to group several extracted values together into a case-class
instance. instance.

View file

@ -25,7 +25,7 @@ to use which shows properties of different parameter directives.
directive level ordering multi directive level ordering multi
========================== ====== ======== ===== ========================== ====== ======== =====
:ref:`-parameter-` high no no :ref:`-parameter-` high no no
:ref:`-parameters-` high no no :ref:`-parameters-` high no yes
:ref:`-parameterMap-` low no no :ref:`-parameterMap-` low no no
:ref:`-parameterMultiMap-` low no yes :ref:`-parameterMultiMap-` low no yes
:ref:`-parameterSeq-` low yes yes :ref:`-parameterSeq-` low yes yes

View file

@ -24,7 +24,7 @@ Description
----------- -----------
Query parameters can be either extracted as a String or can be converted to another type. The parameter name Query parameters can be either extracted as a String or can be converted to another type. The parameter name
can be supplied either as a String or as a Symbol. Parameter extraction can be modified to mark a query parameter can be supplied either as a String or as a Symbol. Parameter extraction can be modified to mark a query parameter
as required or optional or to filter requests where a parameter has a certain value: as required, optional, or repeated, or to filter requests where a parameter has a certain value:
``"color"`` ``"color"``
extract value of parameter "color" as ``String`` extract value of parameter "color" as ``String``
@ -39,6 +39,13 @@ as required or optional or to filter requests where a parameter has a certain va
(see also :ref:`http-unmarshalling-scala`) (see also :ref:`http-unmarshalling-scala`)
``"amount".as(deserializer)`` ``"amount".as(deserializer)``
extract value of parameter "amount" with an explicit ``Deserializer`` extract value of parameter "amount" with an explicit ``Deserializer``
``"distance".*``
extract multiple occurrences of parameter "distance" as ``Iterable[String]``
``"distance".as[Int].*``
extract multiple occurrences of parameter "distance" as ``Iterable[Int]``, you need a matching ``Deserializer`` in scope for that to work
(see also :ref:`unmarshalling`)
``"distance".as(deserializer).*``
extract multiple occurrences of parameter "distance" with an explicit ``Deserializer``
You can use :ref:`Case Class Extraction` to group several extracted values together into a case-class You can use :ref:`Case Class Extraction` to group several extracted values together into a case-class
instance. instance.
@ -80,3 +87,15 @@ Deserialized parameter
... includecode2:: ../../../../code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala ... includecode2:: ../../../../code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala
:snippet: mapped-value :snippet: mapped-value
Repeated parameter
+++++++++++++++++++++++++++++
... includecode2:: ../../../../code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala
:snippet: repeated
Repeated, deserialized parameter
++++++++++++++++++++++
... includecode2:: ../../../../code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala
:snippet: mapped-repeated

View file

@ -137,4 +137,26 @@ class FormFieldDirectivesSpec extends RoutingSpec {
} }
} }
"The 'formField' repeated directive" should {
"extract an empty Iterable when the parameter is absent" in {
Post("/", FormData("age" -> "42")) ~> {
formFields('hobby.*) { echoComplete }
} ~> check { responseAs[String] === "List()" }
}
"extract all occurrences into an Iterable when parameter is present" in {
Post("/", FormData("age" -> "42", "hobby" -> "cooking", "hobby" -> "reading")) ~> {
formFields('hobby.*) { echoComplete }
} ~> check { responseAs[String] === "List(cooking, reading)" }
}
"extract as Iterable[Int]" in {
Post("/", FormData("age" -> "42", "number" -> "3", "number" -> "5")) ~> {
formFields('number.as[Int]*) { echoComplete }
} ~> check { responseAs[String] === "List(3, 5)" }
}
"extract as Iterable[Int] with an explicit deserializer" in {
Post("/", FormData("age" -> "42", "number" -> "3", "number" -> "A")) ~> {
formFields('number.as(HexInt)*) { echoComplete }
} ~> check { responseAs[String] === "List(3, 10)" }
}
}
} }

View file

@ -182,4 +182,27 @@ class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Insi
Get() ~> route ~> check { responseAs[String] shouldEqual "GET" } Get() ~> route ~> check { responseAs[String] shouldEqual "GET" }
} }
} }
"The 'parameter' repeated directive should" - {
"extract an empty Iterable when the parameter is absent" in {
Get("/person?age=19") ~> {
parameter('hobby.*) { echoComplete }
} ~> check { responseAs[String] === "List()" }
}
"extract all occurrences into an Iterable when parameter is present" in {
Get("/person?age=19&hobby=cooking&hobby=reading") ~> {
parameter('hobby.*) { echoComplete }
} ~> check { responseAs[String] === "List(cooking, reading)" }
}
"extract as Iterable[Int]" in {
Get("/person?age=19&number=3&number=5") ~> {
parameter('number.as[Int]*) { echoComplete }
} ~> check { responseAs[String] === "List(3, 5)" }
}
"extract as Iterable[Int] with an explicit deserializer" in {
Get("/person?age=19&number=3&number=A") ~> {
parameter('number.as(HexInt)*) { echoComplete }
} ~> check { responseAs[String] === "List(3, 10)" }
}
}
} }

View file

@ -18,12 +18,14 @@ class NameReceptacle[T](val name: String) {
def ? = new NameOptionReceptacle[T](name) def ? = new NameOptionReceptacle[T](name)
def ?[B](default: B) = new NameDefaultReceptacle(name, default) def ?[B](default: B) = new NameDefaultReceptacle(name, default)
def ![B](requiredValue: B) = new RequiredValueReceptacle(name, requiredValue) def ![B](requiredValue: B) = new RequiredValueReceptacle(name, requiredValue)
def * = new RepeatedValueReceptacle[T](name)
} }
class NameUnmarshallerReceptacle[T](val name: String, val um: FSU[T]) { class NameUnmarshallerReceptacle[T](val name: String, val um: FSU[T]) {
def ? = new NameOptionUnmarshallerReceptacle[T](name, um) def ? = new NameOptionUnmarshallerReceptacle[T](name, um)
def ?(default: T) = new NameDefaultUnmarshallerReceptacle(name, default, um) def ?(default: T) = new NameDefaultUnmarshallerReceptacle(name, default, um)
def !(requiredValue: T) = new RequiredValueUnmarshallerReceptacle(name, requiredValue, um) def !(requiredValue: T) = new RequiredValueUnmarshallerReceptacle(name, requiredValue, um)
def * = new RepeatedValueUnmarshallerReceptacle[T](name, um)
} }
class NameOptionReceptacle[T](val name: String) class NameOptionReceptacle[T](val name: String)
@ -32,8 +34,12 @@ class NameDefaultReceptacle[T](val name: String, val default: T)
class RequiredValueReceptacle[T](val name: String, val requiredValue: T) class RequiredValueReceptacle[T](val name: String, val requiredValue: T)
class RepeatedValueReceptacle[T](val name: String)
class NameOptionUnmarshallerReceptacle[T](val name: String, val um: FSU[T]) class NameOptionUnmarshallerReceptacle[T](val name: String, val um: FSU[T])
class NameDefaultUnmarshallerReceptacle[T](val name: String, val default: T, val um: FSU[T]) class NameDefaultUnmarshallerReceptacle[T](val name: String, val default: T, val um: FSU[T])
class RequiredValueUnmarshallerReceptacle[T](val name: String, val requiredValue: T, val um: FSU[T]) class RequiredValueUnmarshallerReceptacle[T](val name: String, val requiredValue: T, val um: FSU[T])
class RepeatedValueUnmarshallerReceptacle[T](val name: String, val um: FSU[T])

View file

@ -61,23 +61,22 @@ object FormFieldDirectives extends FormFieldDirectives {
type SFU = FromEntityUnmarshaller[StrictForm] type SFU = FromEntityUnmarshaller[StrictForm]
type FSFFOU[T] = Unmarshaller[Option[StrictForm.Field], T] type FSFFOU[T] = Unmarshaller[Option[StrictForm.Field], T]
private def extractField[A, B](f: A Directive1[B]): FieldDefAux[A, Directive1[B]] = fieldDef(f)
private def handleFieldResult[T](fieldName: String, result: Future[T]): Directive1[T] = onComplete(result).flatMap {
case Success(x) provide(x)
case Failure(Unmarshaller.NoContentException) reject(MissingFormFieldRejection(fieldName))
case Failure(x: UnsupportedContentTypeException) reject(UnsupportedRequestContentTypeRejection(x.supported))
case Failure(x) reject(MalformedFormFieldRejection(fieldName, x.getMessage.nullAsEmpty, Option(x.getCause)))
}
//////////////////// "regular" formField extraction //////////////////// //////////////////// "regular" formField extraction ////////////////////
private def extractField[A, B](f: A Directive1[B]): FieldDefAux[A, Directive1[B]] = fieldDef(f)
private def fieldOfForm[T](fieldName: String, fu: Unmarshaller[Option[StrictForm.Field], T])(implicit sfu: SFU): RequestContext Future[T] = { ctx private def fieldOfForm[T](fieldName: String, fu: Unmarshaller[Option[StrictForm.Field], T])(implicit sfu: SFU): RequestContext Future[T] = { ctx
import ctx.executionContext import ctx.executionContext
sfu(ctx.request.entity).fast.flatMap(form fu(form field fieldName)) sfu(ctx.request.entity).fast.flatMap(form fu(form field fieldName))
} }
private def filter[T](fieldName: String, fu: FSFFOU[T])(implicit sfu: SFU): Directive1[T] = { private def filter[T](fieldName: String, fu: FSFFOU[T])(implicit sfu: SFU): Directive1[T] =
extract(fieldOfForm(fieldName, fu)).flatMap { extract(fieldOfForm(fieldName, fu)).flatMap(r handleFieldResult(fieldName, r))
onComplete(_).flatMap {
case Success(x) provide(x)
case Failure(Unmarshaller.NoContentException) reject(MissingFormFieldRejection(fieldName))
case Failure(x: UnsupportedContentTypeException) reject(UnsupportedRequestContentTypeRejection(x.supported))
case Failure(x) reject(MalformedFormFieldRejection(fieldName, x.getMessage.nullAsEmpty, Option(x.getCause)))
}
}
}
implicit def forString(implicit sfu: SFU, fu: FSFFU[String]): FieldDefAux[String, Directive1[String]] = implicit def forString(implicit sfu: SFU, fu: FSFFU[String]): FieldDefAux[String, Directive1[String]] =
extractField[String, String] { fieldName filter(fieldName, fu) } extractField[String, String] { fieldName filter(fieldName, fu) }
implicit def forSymbol(implicit sfu: SFU, fu: FSFFU[String]): FieldDefAux[Symbol, Directive1[String]] = implicit def forSymbol(implicit sfu: SFU, fu: FSFFU[String]): FieldDefAux[Symbol, Directive1[String]] =
@ -110,6 +109,20 @@ object FormFieldDirectives extends FormFieldDirectives {
implicit def forRVDR[T](implicit sfu: SFU): FieldDefAux[RequiredValueUnmarshallerReceptacle[T], Directive0] = implicit def forRVDR[T](implicit sfu: SFU): FieldDefAux[RequiredValueUnmarshallerReceptacle[T], Directive0] =
fieldDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr requiredFilter(rvr.name, StrictForm.Field.unmarshallerFromFSU(rvr.um), rvr.requiredValue) } fieldDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr requiredFilter(rvr.name, StrictForm.Field.unmarshallerFromFSU(rvr.um), rvr.requiredValue) }
//////////////////// repeated formField support ////////////////////
private def repeatedFilter[T](fieldName: String, fu: FSFFU[T])(implicit sfu: SFU): Directive1[Iterable[T]] =
extract { ctx
import ctx.executionContext
sfu(ctx.request.entity).fast.flatMap(form Future.sequence(form.fields.collect { case (`fieldName`, value) fu(value) }))
}.flatMap { result
handleFieldResult(fieldName, result)
}
implicit def forRepVR[T](implicit sfu: SFU, fu: FSFFU[T]): FieldDefAux[RepeatedValueReceptacle[T], Directive1[Iterable[T]]] =
extractField[RepeatedValueReceptacle[T], Iterable[T]] { rvr repeatedFilter(rvr.name, fu) }
implicit def forRepVDR[T](implicit sfu: SFU): FieldDefAux[RepeatedValueUnmarshallerReceptacle[T], Directive1[Iterable[T]]] =
extractField[RepeatedValueUnmarshallerReceptacle[T], Iterable[T]] { rvr repeatedFilter(rvr.name, StrictForm.Field.unmarshallerFromFSU(rvr.um)) }
//////////////////// tuple support //////////////////// //////////////////// tuple support ////////////////////
import akka.http.scaladsl.server.util.TupleOps._ import akka.http.scaladsl.server.util.TupleOps._

View file

@ -6,6 +6,7 @@ package akka.http.scaladsl.server
package directives package directives
import scala.collection.immutable import scala.collection.immutable
import scala.concurrent.{ ExecutionContext, Future }
import scala.util.{ Failure, Success } import scala.util.{ Failure, Success }
import akka.http.scaladsl.common._ import akka.http.scaladsl.common._
import akka.http.impl.util._ import akka.http.impl.util._
@ -84,17 +85,20 @@ object ParameterDirectives extends ParameterDirectives {
import FutureDirectives._ import FutureDirectives._
type FSOU[T] = Unmarshaller[Option[String], T] type FSOU[T] = Unmarshaller[Option[String], T]
private def extractParameter[A, B](f: A Directive1[B]): ParamDefAux[A, Directive1[B]] = paramDef(f)
private def handleParamResult[T](paramName: String, result: Future[T])(implicit ec: ExecutionContext): Directive1[T] =
onComplete(result).flatMap {
case Success(x) provide(x)
case Failure(Unmarshaller.NoContentException) reject(MissingQueryParamRejection(paramName))
case Failure(x) reject(MalformedQueryParamRejection(paramName, x.getMessage.nullAsEmpty, Option(x.getCause)))
}
//////////////////// "regular" parameter extraction ////////////////////// //////////////////// "regular" parameter extraction //////////////////////
private def extractParameter[A, B](f: A Directive1[B]): ParamDefAux[A, Directive1[B]] = paramDef(f)
private def filter[T](paramName: String, fsou: FSOU[T]): Directive1[T] = private def filter[T](paramName: String, fsou: FSOU[T]): Directive1[T] =
extractRequestContext flatMap { ctx extractRequestContext flatMap { ctx
import ctx.executionContext import ctx.executionContext
onComplete(fsou(ctx.request.uri.query get paramName)) flatMap { handleParamResult(paramName, fsou(ctx.request.uri.query get paramName))
case Success(x) provide(x)
case Failure(Unmarshaller.NoContentException) reject(MissingQueryParamRejection(paramName))
case Failure(x) reject(MalformedQueryParamRejection(paramName, x.getMessage.nullAsEmpty, Option(x.getCause)))
}
} }
implicit def forString(implicit fsu: FSU[String]): ParamDefAux[String, Directive1[String]] = implicit def forString(implicit fsu: FSU[String]): ParamDefAux[String, Directive1[String]] =
extractParameter[String, String] { string filter(string, fsu) } extractParameter[String, String] { string filter(string, fsu) }
@ -128,6 +132,18 @@ object ParameterDirectives extends ParameterDirectives {
implicit def forRVDR[T]: ParamDefAux[RequiredValueUnmarshallerReceptacle[T], Directive0] = implicit def forRVDR[T]: ParamDefAux[RequiredValueUnmarshallerReceptacle[T], Directive0] =
paramDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr requiredFilter(rvr.name, rvr.um, rvr.requiredValue) } paramDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr requiredFilter(rvr.name, rvr.um, rvr.requiredValue) }
//////////////////// repeated parameter support ////////////////////
private def repeatedFilter[T](paramName: String, fsu: FSU[T]): Directive1[Iterable[T]] =
extractRequestContext flatMap { ctx
import ctx.executionContext
handleParamResult(paramName, Future.sequence(ctx.request.uri.query.getAll(paramName).map(fsu.apply)))
}
implicit def forRepVR[T](implicit fsu: FSU[T]): ParamDefAux[RepeatedValueReceptacle[T], Directive1[Iterable[T]]] =
extractParameter[RepeatedValueReceptacle[T], Iterable[T]] { rvr repeatedFilter(rvr.name, fsu) }
implicit def forRepVDR[T]: ParamDefAux[RepeatedValueUnmarshallerReceptacle[T], Directive1[Iterable[T]]] =
extractParameter[RepeatedValueUnmarshallerReceptacle[T], Iterable[T]] { rvr repeatedFilter(rvr.name, rvr.um) }
//////////////////// tuple support //////////////////// //////////////////// tuple support ////////////////////
import akka.http.scaladsl.server.util.TupleOps._ import akka.http.scaladsl.server.util.TupleOps._