diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFieldMap.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFieldMap.rst new file mode 100644 index 0000000000..fc20e6492e --- /dev/null +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFieldMap.rst @@ -0,0 +1,29 @@ +.. _-formFieldMap-: + +formFieldMap +============ + +Signature +--------- + +.. includecode2:: /../../akka-http/src/main/scala/akka/http/scaladsl/server/directives/FormFieldDirectives.scala + :snippet: formFieldMap + +Description +----------- +Extracts all HTTP form fields at once as a ``Map[String, String]`` mapping form field names to form field values. + +If form data contain a field value several times, the map will contain the last one. + +See :ref:`-formFields-` for an in-depth description. + +Warning +------- +Use of this directive can result in performance degradation or even in ``OutOfMemoryError`` s. +See :ref:`-formFieldSeq-` for details. + +Example +------- + +.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/FormFieldDirectivesExamplesSpec.scala + :snippet: formFieldMap diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFieldMultiMap.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFieldMultiMap.rst new file mode 100644 index 0000000000..3f023484e4 --- /dev/null +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFieldMultiMap.rst @@ -0,0 +1,33 @@ +.. _-formFieldMultiMap-: + +formFieldMultiMap +================= + +Signature +--------- + +.. includecode2:: /../../akka-http/src/main/scala/akka/http/scaladsl/server/directives/FormFieldDirectives.scala + :snippet: formFieldMultiMap + +Description +----------- + +Extracts all HTTP form fields at once as a multi-map of type ``Map[String, List[String]`` mapping +a form name to a list of all its values. + +This directive can be used if form fields can occur several times. + +The order of values is *not* specified. + +See :ref:`-formFields-` for an in-depth description. + +Warning +------- +Use of this directive can result in performance degradation or even in ``OutOfMemoryError`` s. +See :ref:`-formFieldSeq-` for details. + +Example +------- + +.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/FormFieldDirectivesExamplesSpec.scala + :snippet: formFieldMultiMap diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFieldSeq.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFieldSeq.rst new file mode 100644 index 0000000000..3ff16bb611 --- /dev/null +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/form-field-directives/formFieldSeq.rst @@ -0,0 +1,30 @@ +.. _-formFieldSeq-: + +formFieldSeq +============ + +Signature +--------- + +.. includecode2:: /../../akka-http/src/main/scala/akka/http/scaladsl/server/directives/FormFieldDirectives.scala + :snippet: formFieldSeq + +Description +----------- +Extracts all HTTP form fields at once in the original order as (name, value) tuples of type ``(String, String)``. + +This directive can be used if the exact order of form fields is important or if parameters can occur several times. + +See :ref:`-formFields-` for an in-depth description. + +Warning +------- +The directive reads all incoming HTT form fields without any configured upper bound. +It means, that requests with form fields holding significant amount of data (ie. during a file upload) +can cause performance issues or even an ``OutOfMemoryError`` s. + +Example +------- + +.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/FormFieldDirectivesExamplesSpec.scala + :snippet: formFieldSeq diff --git a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/FormFieldDirectivesExamplesSpec.scala b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/FormFieldDirectivesExamplesSpec.scala index cb4ed72944..d4cd5f1b5a 100644 --- a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/FormFieldDirectivesExamplesSpec.scala +++ b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/FormFieldDirectivesExamplesSpec.scala @@ -44,5 +44,53 @@ class FormFieldDirectivesExamplesSpec extends RoutingSpec { responseAs[String] shouldEqual "Request is missing required form field 'color'" } } + "formFieldMap" in { + val route = + formFieldMap { fields => + def formFieldString(formField: (String, String)): String = + s"""${formField._1} = '${formField._2}'""" + complete(s"The form fields are ${fields.map(formFieldString).mkString(", ")}") + } + + // tests: + Post("/", FormData("color" -> "blue", "count" -> "42")) ~> route ~> check { + responseAs[String] shouldEqual "The form fields are color = 'blue', count = '42'" + } + Post("/", FormData("x" -> "1", "x" -> "5")) ~> route ~> check { + responseAs[String] shouldEqual "The form fields are x = '5'" + } + } + "formFieldMultiMap" in { + val route = + formFieldMultiMap { fields => + complete("There are " + + s"form fields ${fields.map(x => x._1 + " -> " + x._2.size).mkString(", ")}") + } + + // tests: + Post("/", FormData("color" -> "blue", "count" -> "42")) ~> route ~> check { + responseAs[String] shouldEqual "There are form fields color -> 1, count -> 1" + } + Post("/", FormData("x" -> "23", "x" -> "4", "x" -> "89")) ~> route ~> check { + responseAs[String] shouldEqual "There are form fields x -> 3" + } + } + "formFieldSeq" in { + val route = + formFieldSeq { fields => + def formFieldString(formField: (String, String)): String = + s"""${formField._1} = '${formField._2}'""" + complete(s"The form fields are ${fields.map(formFieldString).mkString(", ")}") + } + + // tests: + Post("/", FormData("color" -> "blue", "count" -> "42")) ~> route ~> check { + responseAs[String] shouldEqual "The form fields are color = 'blue', count = '42'" + } + Post("/", FormData("x" -> "23", "x" -> "4", "x" -> "89")) ~> route ~> check { + responseAs[String] shouldEqual "The form fields are x = '23', x = '4', x = '89'" + } + } + } diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst b/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst index 86a93aa8c4..10f2055570 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst @@ -64,7 +64,13 @@ Directive Description closest :ref:`-handleExceptions-` directive and its ``ExceptionHandler`` :ref:`-fileUpload-` Provides a stream of an uploaded file from a multipart request :ref:`-formField-` Extracts an HTTP form field from the request +:ref:`-formFieldMap-` Extracts a number of HTTP form field from the request as + a ``Map[String, String]`` +:ref:`-formFieldMultiMap-` Extracts a number of HTTP form field from the request as + a ``Map[String, List[String]`` :ref:`-formFields-` Extracts a number of HTTP form field from the request +:ref:`-formFieldSeq-` Extracts a number of HTTP form field from the request as + a ``Seq[(String, String)]`` :ref:`-get-` Rejects all non-GET requests :ref:`-getFromBrowseableDirectories-` Serves the content of the given directories as a file-system browser, i.e. files are sent and directories served as browseable listings diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/form-field-directives/index.rst b/akka-docs/rst/scala/http/routing-dsl/directives/form-field-directives/index.rst index 80c461f02f..416da4ee44 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/form-field-directives/index.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/form-field-directives/index.rst @@ -7,4 +7,7 @@ FormFieldDirectives :maxdepth: 1 formField - formFields \ No newline at end of file + formFields + formFieldSeq + formFieldMap + formFieldMultiMap \ No newline at end of file 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 f92f634066..43a1d741a2 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 @@ -162,4 +162,33 @@ class FormFieldDirectivesSpec extends RoutingSpec { } ~> check { responseAs[String] === "List(3, 10)" } } } + + "The 'formFieldMap' directive" should { + "extract fields with different keys" in { + Post("/", FormData("age" -> "42", "numberA" -> "3", "numberB" -> "5")) ~> { + formFieldMap { echoComplete } + } ~> check { responseAs[String] shouldEqual "Map(age -> 42, numberA -> 3, numberB -> 5)" } + } + } + + "The 'formFieldSeq' directive" should { + "extract all fields" in { + Post("/", FormData("age" -> "42", "number" -> "3", "number" -> "5")) ~> { + formFieldSeq { echoComplete } + } ~> check { responseAs[String] shouldEqual "Vector((age,42), (number,3), (number,5))" } + } + "produce empty Seq when FormData is empty" in { + Post("/", FormData.Empty) ~> { + formFieldSeq { echoComplete } + } ~> check { responseAs[String] shouldEqual "Vector()" } + } + } + + "The 'formFieldMultiMap' directive" should { + "extract fields with different keys (with duplicates)" in { + Post("/", FormData("age" -> "42", "number" -> "3", "number" -> "5")) ~> { + formFieldMultiMap { echoComplete } + } ~> check { responseAs[String] shouldEqual "Map(age -> List(42), number -> List(5, 3))" } + } + } } 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 e856ebbeca..08da4828ae 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 @@ -5,16 +5,35 @@ package akka.http.scaladsl.server package directives +import akka.http.impl.util._ +import akka.http.scaladsl.common._ +import akka.http.scaladsl.server.directives.RouteDirectives._ +import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException +import akka.http.scaladsl.util.FastFuture._ + +import scala.annotation.tailrec +import scala.collection.immutable import scala.concurrent.Future import scala.util.{ Failure, Success } -import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException -import akka.http.scaladsl.common._ -import akka.http.impl.util._ -import akka.http.scaladsl.util.FastFuture._ trait FormFieldDirectives extends ToNameReceptacleEnhancements { import FormFieldDirectives._ + /** + * Extracts HTTP form fields from the request as a ``Map[String, String]``. + */ + def formFieldMap: Directive1[Map[String, String]] = _formFieldMap + + /** + * Extracts HTTP form fields from the request as a ``Map[String, List[String]]``. + */ + def formFieldMultiMap: Directive1[Map[String, List[String]]] = _formFieldMultiMap + + /** + * Extracts HTTP form fields from the request as a ``Seq[(String, String)]``. + */ + def formFieldSeq: Directive1[immutable.Seq[(String, String)]] = _formFieldSeq + /** * Extracts an HTTP form field from the request. * Rejects the request if the defined form field matcher(s) don't match. @@ -40,6 +59,50 @@ trait FormFieldDirectives extends ToNameReceptacleEnhancements { } object FormFieldDirectives extends FormFieldDirectives { + + private val _formFieldSeq: Directive1[immutable.Seq[(String, String)]] = { + import BasicDirectives._ + import FutureDirectives._ + import akka.http.scaladsl.unmarshalling._ + + extract { ctx ⇒ + import ctx.{ executionContext, materializer } + Unmarshal(ctx.request.entity).to[StrictForm].fast.flatMap { form ⇒ + val fields = form.fields.collect { + case (name, field) if name.nonEmpty ⇒ + Unmarshal(field).to[String].map(fieldString ⇒ (name, fieldString)) + } + Future.sequence(fields) + } + }.flatMap { sequenceF ⇒ + onComplete(sequenceF).flatMap { + case Success(x) ⇒ provide(x) + case Failure(x: UnsupportedContentTypeException) ⇒ reject(UnsupportedRequestContentTypeRejection(x.supported)) + case Failure(_) ⇒ reject // TODO Use correct rejections + } + } + } + + private val _formFieldMultiMap: Directive1[Map[String, List[String]]] = { + @tailrec def append( + map: Map[String, List[String]], + fields: immutable.Seq[(String, String)]): Map[String, List[String]] = { + if (fields.isEmpty) { + map + } else { + val (key, value) = fields.head + append(map.updated(key, value :: map.getOrElse(key, Nil)), fields.tail) + } + } + + _formFieldSeq.map { + case seq ⇒ + append(Map.empty, seq) + } + } + + private val _formFieldMap: Directive1[Map[String, String]] = _formFieldSeq.map(_.toMap) + sealed trait FieldMagnet { type Out def apply(): Out @@ -64,10 +127,10 @@ object FormFieldDirectives extends FormFieldDirectives { def apply(value: A) = f(value) } - import akka.http.scaladsl.unmarshalling.{ FromStrictFormFieldUnmarshaller ⇒ FSFFU, _ } import BasicDirectives._ - import RouteDirectives._ import FutureDirectives._ + import RouteDirectives._ + import akka.http.scaladsl.unmarshalling.{ FromStrictFormFieldUnmarshaller ⇒ FSFFU, _ } type SFU = FromEntityUnmarshaller[StrictForm] type FSFFOU[T] = Unmarshaller[Option[StrictForm.Field], T] @@ -82,8 +145,7 @@ object FormFieldDirectives extends FormFieldDirectives { //////////////////// "regular" formField extraction //////////////////// private def fieldOfForm[T](fieldName: String, fu: Unmarshaller[Option[StrictForm.Field], T])(implicit sfu: SFU): RequestContext ⇒ Future[T] = { ctx ⇒ - import ctx.executionContext - import ctx.materializer + import ctx.{ executionContext, materializer } 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] = @@ -124,8 +186,7 @@ object FormFieldDirectives extends FormFieldDirectives { private def repeatedFilter[T](fieldName: String, fu: FSFFU[T])(implicit sfu: SFU): Directive1[Iterable[T]] = extract { ctx ⇒ - import ctx.executionContext - import ctx.materializer + import ctx.{ executionContext, materializer } sfu(ctx.request.entity).fast.flatMap(form ⇒ Future.sequence(form.fields.collect { case (`fieldName`, value) ⇒ fu(value) })) }.flatMap { result ⇒ handleFieldResult(fieldName, result) @@ -137,8 +198,8 @@ object FormFieldDirectives extends FormFieldDirectives { //////////////////// tuple support //////////////////// - import akka.http.scaladsl.server.util.TupleOps._ import akka.http.scaladsl.server.util.BinaryPolyFunc + import akka.http.scaladsl.server.util.TupleOps._ implicit def forTuple[T](implicit fold: FoldLeft[Directive0, T, ConvertFieldDefAndConcatenate.type]): FieldDefAux[T, fold.Out] = fieldDef[T, fold.Out](fold(pass, _))