Merge pull request #19187 from 2beaucoup/generic-csv-form-fields
!htp #19185 generic CSV form field unmarshallers
This commit is contained in:
commit
d7b45e0fc3
4 changed files with 45 additions and 123 deletions
|
|
@ -192,13 +192,16 @@ class ParameterDirectivesExamplesSpec extends RoutingSpec with PredefinedFromStr
|
||||||
responseAs[String] shouldEqual "The parameters are x = '1', x = '2'"
|
responseAs[String] shouldEqual "The parameters are x = '1', x = '2'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"csv string sequence" in {
|
"csv" in {
|
||||||
val route =
|
val route =
|
||||||
parameter("names".as(CsvStringSeq)) { names =>
|
parameter("names".as(CsvSeq[String])) { names =>
|
||||||
complete(s"The parameters are ${names.mkString(", ")}")
|
complete(s"The parameters are ${names.mkString(", ")}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// tests:
|
// tests:
|
||||||
|
Get("/?names=") ~> route ~> check {
|
||||||
|
responseAs[String] shouldEqual "The parameters are "
|
||||||
|
}
|
||||||
Get("/?names=Caplin") ~> route ~> check {
|
Get("/?names=Caplin") ~> route ~> check {
|
||||||
responseAs[String] shouldEqual "The parameters are Caplin"
|
responseAs[String] shouldEqual "The parameters are Caplin"
|
||||||
}
|
}
|
||||||
|
|
@ -206,48 +209,4 @@ class ParameterDirectivesExamplesSpec extends RoutingSpec with PredefinedFromStr
|
||||||
responseAs[String] shouldEqual "The parameters are Caplin, John"
|
responseAs[String] shouldEqual "The parameters are Caplin, John"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"csv byte sequence" in {
|
|
||||||
val route =
|
|
||||||
parameter("numbers".as(CsvByteSeq)) { bytes =>
|
|
||||||
complete(s"The numbers are ${bytes.mkString(", ")}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// tests:
|
|
||||||
Get(s"/?numbers=2,${Byte.MaxValue}") ~> route ~> check {
|
|
||||||
responseAs[String] shouldEqual s"The numbers are 2, ${Byte.MaxValue}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"csv short sequence" in {
|
|
||||||
val route =
|
|
||||||
parameter("numbers".as(CsvShortSeq)) { shorts =>
|
|
||||||
complete(s"The numbers are ${shorts.mkString(", ")}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// tests:
|
|
||||||
Get(s"/?numbers=2,${Short.MaxValue}") ~> route ~> check {
|
|
||||||
responseAs[String] shouldEqual s"The numbers are 2, ${Short.MaxValue}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"csv int sequence" in {
|
|
||||||
val route =
|
|
||||||
parameter("numbers".as(CsvIntSeq)) { ints =>
|
|
||||||
complete(s"The numbers are ${ints.mkString(", ")}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// tests:
|
|
||||||
Get(s"/?numbers=2,${Int.MaxValue}") ~> route ~> check {
|
|
||||||
responseAs[String] shouldEqual s"The numbers are 2, ${Int.MaxValue}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"csv long sequence" in {
|
|
||||||
val route =
|
|
||||||
parameter("numbers".as(CsvLongSeq)) { longs =>
|
|
||||||
complete(s"The numbers are ${longs.mkString(", ")}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// tests:
|
|
||||||
Get(s"/?numbers=2,${Long.MaxValue}") ~> route ~> check {
|
|
||||||
responseAs[String] shouldEqual s"The numbers are 2, ${Long.MaxValue}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,8 @@ Repeated parameter
|
||||||
.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala
|
.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala
|
||||||
:snippet: repeated
|
:snippet: repeated
|
||||||
|
|
||||||
Csv valued parameter
|
CSV parameter
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala
|
.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/ParameterDirectivesExamplesSpec.scala
|
||||||
:snippet: csv
|
:snippet: csv
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@ package akka.http.scaladsl.server
|
||||||
package directives
|
package directives
|
||||||
|
|
||||||
import org.scalatest.{ FreeSpec, Inside }
|
import org.scalatest.{ FreeSpec, Inside }
|
||||||
import akka.http.scaladsl.unmarshalling.Unmarshaller.HexInt
|
import akka.http.scaladsl.unmarshalling.Unmarshaller._
|
||||||
import akka.http.scaladsl.unmarshalling.Unmarshaller.CsvStringSeq
|
|
||||||
|
|
||||||
class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Inside {
|
class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Inside {
|
||||||
"when used with 'as[Int]' the parameter directive should" - {
|
"when used with 'as[Int]' the parameter directive should" - {
|
||||||
|
|
@ -33,17 +32,17 @@ class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Insi
|
||||||
"create typed optional parameters that" - {
|
"create typed optional parameters that" - {
|
||||||
"extract Some(value) when present" in {
|
"extract Some(value) when present" in {
|
||||||
Get("/?amount=12") ~> {
|
Get("/?amount=12") ~> {
|
||||||
parameter("amount".as[Int]?) { echoComplete }
|
parameter("amount".as[Int].?) { echoComplete }
|
||||||
} ~> check { responseAs[String] shouldEqual "Some(12)" }
|
} ~> check { responseAs[String] shouldEqual "Some(12)" }
|
||||||
}
|
}
|
||||||
"extract None when not present" in {
|
"extract None when not present" in {
|
||||||
Get() ~> {
|
Get() ~> {
|
||||||
parameter("amount".as[Int]?) { echoComplete }
|
parameter("amount".as[Int].?) { echoComplete }
|
||||||
} ~> check { responseAs[String] shouldEqual "None" }
|
} ~> check { responseAs[String] shouldEqual "None" }
|
||||||
}
|
}
|
||||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||||
Get("/?amount=x") ~> {
|
Get("/?amount=x") ~> {
|
||||||
parameter("amount".as[Int]?) { echoComplete }
|
parameter("amount".as[Int].?) { echoComplete }
|
||||||
} ~> check {
|
} ~> check {
|
||||||
inside(rejection) {
|
inside(rejection) {
|
||||||
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit signed integer value", Some(_)) ⇒
|
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit signed integer value", Some(_)) ⇒
|
||||||
|
|
@ -53,9 +52,9 @@ class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Insi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"when used with 'as(CsvStringSeq)' the parameter directive should" - {
|
"when used with 'as(CsvSeq[...])' the parameter directive should" - {
|
||||||
val route =
|
val route =
|
||||||
parameter("names".as(CsvStringSeq)) { names ⇒
|
parameter("names".as(CsvSeq[String])) { names ⇒
|
||||||
complete(s"The parameters are ${names.mkString(", ")}")
|
complete(s"The parameters are ${names.mkString(", ")}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,17 +93,17 @@ class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Insi
|
||||||
"create typed optional parameters that" - {
|
"create typed optional parameters that" - {
|
||||||
"extract Some(value) when present" in {
|
"extract Some(value) when present" in {
|
||||||
Get("/?amount=A") ~> {
|
Get("/?amount=A") ~> {
|
||||||
parameter("amount".as(HexInt)?) { echoComplete }
|
parameter("amount".as(HexInt).?) { echoComplete }
|
||||||
} ~> check { responseAs[String] shouldEqual "Some(10)" }
|
} ~> check { responseAs[String] shouldEqual "Some(10)" }
|
||||||
}
|
}
|
||||||
"extract None when not present" in {
|
"extract None when not present" in {
|
||||||
Get() ~> {
|
Get() ~> {
|
||||||
parameter("amount".as(HexInt)?) { echoComplete }
|
parameter("amount".as(HexInt).?) { echoComplete }
|
||||||
} ~> check { responseAs[String] shouldEqual "None" }
|
} ~> check { responseAs[String] shouldEqual "None" }
|
||||||
}
|
}
|
||||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||||
Get("/?amount=x") ~> {
|
Get("/?amount=x") ~> {
|
||||||
parameter("amount".as(HexInt)?) { echoComplete }
|
parameter("amount".as(HexInt).?) { echoComplete }
|
||||||
} ~> check {
|
} ~> check {
|
||||||
inside(rejection) {
|
inside(rejection) {
|
||||||
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit hexadecimal integer value", Some(_)) ⇒
|
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit hexadecimal integer value", Some(_)) ⇒
|
||||||
|
|
@ -148,8 +147,8 @@ class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Insi
|
||||||
} ~> check { responseAs[String] shouldEqual "EllenParsons" }
|
} ~> check { responseAs[String] shouldEqual "EllenParsons" }
|
||||||
}
|
}
|
||||||
"correctly extract an optional parameter" in {
|
"correctly extract an optional parameter" in {
|
||||||
Get("/?foo=bar") ~> parameters('foo ?) { echoComplete } ~> check { responseAs[String] shouldEqual "Some(bar)" }
|
Get("/?foo=bar") ~> parameters('foo.?) { echoComplete } ~> check { responseAs[String] shouldEqual "Some(bar)" }
|
||||||
Get("/?foo=bar") ~> parameters('baz ?) { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
|
Get("/?foo=bar") ~> parameters('baz.?) { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
|
||||||
}
|
}
|
||||||
"ignore additional parameters" in {
|
"ignore additional parameters" in {
|
||||||
Get("/?name=Parsons&FirstName=Ellen&age=29") ~> {
|
Get("/?name=Parsons&FirstName=Ellen&age=29") ~> {
|
||||||
|
|
@ -167,7 +166,7 @@ class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Insi
|
||||||
}
|
}
|
||||||
"supply the default value if an optional parameter is missing" in {
|
"supply the default value if an optional parameter is missing" in {
|
||||||
Get("/?name=Parsons&FirstName=Ellen") ~> {
|
Get("/?name=Parsons&FirstName=Ellen") ~> {
|
||||||
parameters("name"?, 'FirstName, 'age ? "29", 'eyes?) { (name, firstName, age, eyes) ⇒
|
parameters("name".?, 'FirstName, 'age ? "29", 'eyes.?) { (name, firstName, age, eyes) ⇒
|
||||||
complete(firstName + name + age + eyes)
|
complete(firstName + name + age + eyes)
|
||||||
}
|
}
|
||||||
} ~> check { responseAs[String] shouldEqual "EllenSome(Parsons)29None" }
|
} ~> check { responseAs[String] shouldEqual "EllenSome(Parsons)29None" }
|
||||||
|
|
@ -214,12 +213,12 @@ class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Insi
|
||||||
}
|
}
|
||||||
"extract as Iterable[Int]" in {
|
"extract as Iterable[Int]" in {
|
||||||
Get("/person?age=19&number=3&number=5") ~> {
|
Get("/person?age=19&number=3&number=5") ~> {
|
||||||
parameter('number.as[Int]*) { echoComplete }
|
parameter('number.as[Int].*) { echoComplete }
|
||||||
} ~> check { responseAs[String] === "List(3, 5)" }
|
} ~> check { responseAs[String] === "List(3, 5)" }
|
||||||
}
|
}
|
||||||
"extract as Iterable[Int] with an explicit deserializer" in {
|
"extract as Iterable[Int] with an explicit deserializer" in {
|
||||||
Get("/person?age=19&number=3&number=A") ~> {
|
Get("/person?age=19&number=3&number=A") ~> {
|
||||||
parameter('number.as(HexInt)*) { echoComplete }
|
parameter('number.as(HexInt).*) { echoComplete }
|
||||||
} ~> check { responseAs[String] === "List(3, 10)" }
|
} ~> check { responseAs[String] === "List(3, 10)" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,56 +5,27 @@
|
||||||
package akka.http.scaladsl.unmarshalling
|
package akka.http.scaladsl.unmarshalling
|
||||||
|
|
||||||
import scala.collection.immutable
|
import scala.collection.immutable
|
||||||
|
import akka.http.scaladsl.util.FastFuture
|
||||||
|
|
||||||
trait PredefinedFromStringUnmarshallers {
|
trait PredefinedFromStringUnmarshallers {
|
||||||
|
|
||||||
implicit val byteFromStringUnmarshaller: Unmarshaller[String, Byte] =
|
implicit val byteFromStringUnmarshaller: Unmarshaller[String, Byte] =
|
||||||
Unmarshaller.strict(byteFromString)
|
numberUnmarshaller(_.toByte, "8-bit signed integer")
|
||||||
|
|
||||||
implicit val shortFromStringUnmarshaller: Unmarshaller[String, Short] =
|
implicit val shortFromStringUnmarshaller: Unmarshaller[String, Short] =
|
||||||
Unmarshaller.strict(shortFromString)
|
numberUnmarshaller(_.toShort, "16-bit signed integer")
|
||||||
|
|
||||||
implicit val intFromStringUnmarshaller: Unmarshaller[String, Int] =
|
implicit val intFromStringUnmarshaller: Unmarshaller[String, Int] =
|
||||||
Unmarshaller.strict(intFromString)
|
numberUnmarshaller(_.toInt, "32-bit signed integer")
|
||||||
|
|
||||||
implicit val longFromStringUnmarshaller: Unmarshaller[String, Long] =
|
implicit val longFromStringUnmarshaller: Unmarshaller[String, Long] =
|
||||||
Unmarshaller.strict(longFromString)
|
numberUnmarshaller(_.toLong, "64-bit signed integer")
|
||||||
|
|
||||||
val HexByte: Unmarshaller[String, Byte] =
|
|
||||||
Unmarshaller.strict[String, Byte] { string ⇒
|
|
||||||
try java.lang.Byte.parseByte(string, 16)
|
|
||||||
catch numberFormatError(string, "8-bit hexadecimal integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
val HexShort: Unmarshaller[String, Short] =
|
|
||||||
Unmarshaller.strict[String, Short] { string ⇒
|
|
||||||
try java.lang.Short.parseShort(string, 16)
|
|
||||||
catch numberFormatError(string, "16-bit hexadecimal integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
val HexInt: Unmarshaller[String, Int] =
|
|
||||||
Unmarshaller.strict[String, Int] { string ⇒
|
|
||||||
try java.lang.Integer.parseInt(string, 16)
|
|
||||||
catch numberFormatError(string, "32-bit hexadecimal integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
val HexLong: Unmarshaller[String, Long] =
|
|
||||||
Unmarshaller.strict[String, Long] { string ⇒
|
|
||||||
try java.lang.Long.parseLong(string, 16)
|
|
||||||
catch numberFormatError(string, "64-bit hexadecimal integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
implicit val floatFromStringUnmarshaller: Unmarshaller[String, Float] =
|
implicit val floatFromStringUnmarshaller: Unmarshaller[String, Float] =
|
||||||
Unmarshaller.strict[String, Float] { string ⇒
|
numberUnmarshaller(_.toFloat, "32-bit floating point")
|
||||||
try string.toFloat
|
|
||||||
catch numberFormatError(string, "32-bit floating point")
|
|
||||||
}
|
|
||||||
|
|
||||||
implicit val doubleFromStringUnmarshaller: Unmarshaller[String, Double] =
|
implicit val doubleFromStringUnmarshaller: Unmarshaller[String, Double] =
|
||||||
Unmarshaller.strict[String, Double] { string ⇒
|
numberUnmarshaller(_.toDouble, "64-bit floating point")
|
||||||
try string.toDouble
|
|
||||||
catch numberFormatError(string, "64-bit floating point")
|
|
||||||
}
|
|
||||||
|
|
||||||
implicit val booleanFromStringUnmarshaller: Unmarshaller[String, Boolean] =
|
implicit val booleanFromStringUnmarshaller: Unmarshaller[String, Boolean] =
|
||||||
Unmarshaller.strict[String, Boolean] { string ⇒
|
Unmarshaller.strict[String, Boolean] { string ⇒
|
||||||
|
|
@ -66,38 +37,31 @@ trait PredefinedFromStringUnmarshallers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val CsvStringSeq: Unmarshaller[String, immutable.Seq[String]] =
|
implicit def CsvSeq[T](implicit unmarshaller: Unmarshaller[String, T]): Unmarshaller[String, immutable.Seq[T]] =
|
||||||
Unmarshaller.strict[String, immutable.Seq[String]] { string ⇒
|
Unmarshaller.strict[String, immutable.Seq[String]] { string ⇒
|
||||||
string.split(",").toList
|
string.split(",").toList
|
||||||
|
} flatMap { implicit ec ⇒
|
||||||
|
implicit mat ⇒ strings ⇒
|
||||||
|
FastFuture.sequence(strings.map(unmarshaller(_)))
|
||||||
}
|
}
|
||||||
|
|
||||||
val CsvByteSeq: Unmarshaller[String, immutable.Seq[Byte]] =
|
val HexByte: Unmarshaller[String, Byte] =
|
||||||
CsvStringSeq.map(_.map(byteFromString))
|
numberUnmarshaller(java.lang.Byte.parseByte(_, 16), "8-bit hexadecimal integer")
|
||||||
|
|
||||||
val CsvShortSeq: Unmarshaller[String, immutable.Seq[Short]] =
|
val HexShort: Unmarshaller[String, Short] =
|
||||||
CsvStringSeq.map(_.map(shortFromString))
|
numberUnmarshaller(java.lang.Short.parseShort(_, 16), "16-bit hexadecimal integer")
|
||||||
|
|
||||||
val CsvIntSeq: Unmarshaller[String, immutable.Seq[Int]] =
|
val HexInt: Unmarshaller[String, Int] =
|
||||||
CsvStringSeq.map(_.map(intFromString))
|
numberUnmarshaller(java.lang.Integer.parseInt(_, 16), "32-bit hexadecimal integer")
|
||||||
|
|
||||||
val CsvLongSeq: Unmarshaller[String, immutable.Seq[Long]] =
|
val HexLong: Unmarshaller[String, Long] =
|
||||||
CsvStringSeq.map(_.map(longFromString))
|
numberUnmarshaller(java.lang.Long.parseLong(_, 16), "64-bit hexadecimal integer")
|
||||||
|
|
||||||
private def byteFromString(string: String) =
|
private def numberUnmarshaller[T](f: String ⇒ T, target: String): Unmarshaller[String, T] =
|
||||||
try string.toByte
|
Unmarshaller.strict[String, T] { string ⇒
|
||||||
catch numberFormatError(string, "8-bit signed integer")
|
try f(string)
|
||||||
|
catch numberFormatError(string, target)
|
||||||
private def shortFromString(string: String) =
|
}
|
||||||
try string.toShort
|
|
||||||
catch numberFormatError(string, "16-bit signed integer")
|
|
||||||
|
|
||||||
private def intFromString(string: String) =
|
|
||||||
try string.toInt
|
|
||||||
catch numberFormatError(string, "32-bit signed integer")
|
|
||||||
|
|
||||||
private def longFromString(string: String) =
|
|
||||||
try string.toLong
|
|
||||||
catch numberFormatError(string, "64-bit signed integer")
|
|
||||||
|
|
||||||
private def numberFormatError(value: String, target: String): PartialFunction[Throwable, Nothing] = {
|
private def numberFormatError(value: String, target: String): PartialFunction[Throwable, Nothing] = {
|
||||||
case e: NumberFormatException ⇒
|
case e: NumberFormatException ⇒
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue