diff --git a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala index a3ae378212..c40c0fb907 100644 --- a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala +++ b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala @@ -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" } } + "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 { val route = parameterMap { params => diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFields.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFields.rst index 7b100d8e84..61d56ff5f9 100644 --- a/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFields.rst +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFields.rst @@ -25,7 +25,7 @@ Description 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 -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"`` 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`) ``"amount".as(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 instance. diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/parameter-directives/index.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/parameter-directives/index.rst index f2009af52b..047ad11852 100644 --- a/akka-docs-dev/rst/scala/http/routing-dsl/directives/parameter-directives/index.rst +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/parameter-directives/index.rst @@ -25,7 +25,7 @@ to use which shows properties of different parameter directives. directive level ordering multi ========================== ====== ======== ===== :ref:`-parameter-` high no no -:ref:`-parameters-` high no no +:ref:`-parameters-` high no yes :ref:`-parameterMap-` low no no :ref:`-parameterMultiMap-` low no yes :ref:`-parameterSeq-` low yes yes diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/parameter-directives/parameters.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/parameter-directives/parameters.rst index 301f06f6d1..ce61d3a424 100644 --- a/akka-docs-dev/rst/scala/http/routing-dsl/directives/parameter-directives/parameters.rst +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/parameter-directives/parameters.rst @@ -24,7 +24,7 @@ Description ----------- 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 -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"`` 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`) ``"amount".as(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 instance. @@ -80,3 +87,15 @@ Deserialized parameter ... includecode2:: ../../../../code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala :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 diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FormFieldDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FormFieldDirectivesSpec.scala index 7b115b3b08..e739248aa3 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FormFieldDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FormFieldDirectivesSpec.scala @@ -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)" } + } + } } diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/ParameterDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/ParameterDirectivesSpec.scala index eded50e662..f96c33baed 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/ParameterDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/ParameterDirectivesSpec.scala @@ -182,4 +182,27 @@ class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Insi 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)" } + } + } } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/common/NameReceptacle.scala b/akka-http/src/main/scala/akka/http/scaladsl/common/NameReceptacle.scala index 42262999c7..41ba493307 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/common/NameReceptacle.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/common/NameReceptacle.scala @@ -18,12 +18,14 @@ class NameReceptacle[T](val name: String) { def ? = new NameOptionReceptacle[T](name) def ?[B](default: B) = new NameDefaultReceptacle(name, default) def ![B](requiredValue: B) = new RequiredValueReceptacle(name, requiredValue) + def * = new RepeatedValueReceptacle[T](name) } class NameUnmarshallerReceptacle[T](val name: String, val um: FSU[T]) { def ? = new NameOptionUnmarshallerReceptacle[T](name, um) def ?(default: T) = new NameDefaultUnmarshallerReceptacle(name, default, um) def !(requiredValue: T) = new RequiredValueUnmarshallerReceptacle(name, requiredValue, um) + def * = new RepeatedValueUnmarshallerReceptacle[T](name, um) } 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 RepeatedValueReceptacle[T](val name: String) + class NameOptionUnmarshallerReceptacle[T](val name: String, 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]) \ No newline at end of file +class RequiredValueUnmarshallerReceptacle[T](val name: String, val requiredValue: T, val um: FSU[T]) + +class RepeatedValueUnmarshallerReceptacle[T](val name: String, val um: FSU[T]) \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FormFieldDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FormFieldDirectives.scala index c027812a98..991366a874 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FormFieldDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FormFieldDirectives.scala @@ -61,23 +61,22 @@ object FormFieldDirectives extends FormFieldDirectives { type SFU = FromEntityUnmarshaller[StrictForm] 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 //////////////////// - 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 ⇒ import ctx.executionContext 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] = { - extract(fieldOfForm(fieldName, fu)).flatMap { - 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))) - } - } - } + private def filter[T](fieldName: String, fu: FSFFOU[T])(implicit sfu: SFU): Directive1[T] = + extract(fieldOfForm(fieldName, fu)).flatMap(r ⇒ handleFieldResult(fieldName, r)) implicit def forString(implicit sfu: SFU, fu: FSFFU[String]): FieldDefAux[String, Directive1[String]] = extractField[String, String] { fieldName ⇒ filter(fieldName, fu) } 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] = 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 //////////////////// import akka.http.scaladsl.server.util.TupleOps._ diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/ParameterDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/ParameterDirectives.scala index 0f007fa336..90b0dfb6fb 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/ParameterDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/ParameterDirectives.scala @@ -6,6 +6,7 @@ package akka.http.scaladsl.server package directives import scala.collection.immutable +import scala.concurrent.{ ExecutionContext, Future } import scala.util.{ Failure, Success } import akka.http.scaladsl.common._ import akka.http.impl.util._ @@ -84,17 +85,20 @@ object ParameterDirectives extends ParameterDirectives { import FutureDirectives._ 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 ////////////////////// - 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] = extractRequestContext flatMap { ctx ⇒ import ctx.executionContext - onComplete(fsou(ctx.request.uri.query get paramName)) 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))) - } + handleParamResult(paramName, fsou(ctx.request.uri.query get paramName)) } implicit def forString(implicit fsu: FSU[String]): ParamDefAux[String, Directive1[String]] = extractParameter[String, String] { string ⇒ filter(string, fsu) } @@ -128,6 +132,18 @@ object ParameterDirectives extends ParameterDirectives { implicit def forRVDR[T]: ParamDefAux[RequiredValueUnmarshallerReceptacle[T], Directive0] = 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 //////////////////// import akka.http.scaladsl.server.util.TupleOps._