+htp: #16651 Add formFieldMap, formFieldMultiMap & formFieldSeq Directives

This commit is contained in:
Wojciech Jurczyk 2015-11-29 00:43:19 +01:00
parent ecc916abfd
commit edf0d6cb21
8 changed files with 251 additions and 12 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'"
}
}
}

View file

@ -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

View file

@ -8,3 +8,6 @@ FormFieldDirectives
formField
formFields
formFieldSeq
formFieldMap
formFieldMultiMap

View file

@ -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))" }
}
}
}

View file

@ -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, _))