Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Commit

Permalink
! httpx: enable FEOU and FSOD to be interchanged in the usual cases, f…
Browse files Browse the repository at this point in the history
…ixes #426

The idea is to
  - interpret application/x-www-form-urlencoded fields as text/plain so that FEOU can read
    it as well
  - interpret multipart form parts with content-type text/plain as strings that
    can be read from FSOD

This should make it much easier to change the form encoding on the client
without having to change anything on the spray side.
  • Loading branch information
jrudolph committed Oct 10, 2013
1 parent 5ac8b64 commit ebaa580
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 18 deletions.
Expand Up @@ -51,15 +51,37 @@ class UrlEncodedFormField(val name: String, val rawValue: Option[String]) extend
type Raw = String
def as[T](implicit ffc: FormFieldConverter[T]) = ffc.urlEncodedFieldConverter match {
case Some(conv) conv(rawValue)
case None fail(name, "multipart/form-data")
case None
ffc.multipartFieldConverter match {
case Some(conv)
conv(rawValue.map(HttpEntity(_))) match {
case Left(UnsupportedContentType(msg))
Left(UnsupportedContentType(msg + " but tried to read from application/x-www-form-urlencoded encoded field '" +
name + "' which provides only text/plain values."))
case x x
}
case None fail(name, "multipart/form-data")
}
}
}

class MultipartFormField(val name: String, val rawValue: Option[BodyPart]) extends FormField {
type Raw = BodyPart
def as[T](implicit ffc: FormFieldConverter[T]) = ffc.multipartFieldConverter match {
case Some(conv) conv(rawValue.map(_.entity))
case None fail(name, "application/x-www-form-urlencoded")
case None
ffc.urlEncodedFieldConverter match {
case Some(conv)
rawValue match {
case Some(BodyPart(HttpEntity.NonEmpty(tpe, data), _)) if tpe.mediaRange.matches(MediaTypes.`text/plain`)
conv(Some(data.asString))
case None | Some(BodyPart(HttpEntity.Empty, _)) conv(None)
case Some(BodyPart(HttpEntity.NonEmpty(tpe, _), _))
Left(UnsupportedContentType(s"Field '$name' can only be read from " +
s"'application/x-www-form-urlencoded' form content but was '${tpe.mediaRange}'"))
}
case None fail(name, "application/x-www-form-urlencoded")
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions spray-httpx/src/test/scala/spray/httpx/FormFieldSpec.scala
Expand Up @@ -61,14 +61,15 @@ class FormFieldSpec extends Specification {
marshal(formData)
.flatMap(_.as[HttpForm])
.flatMap(_.field("age").as[NodeSeq]) ===
Left(UnsupportedContentType("Field 'age' can only be read from 'multipart/form-data' form content"))
Left(UnsupportedContentType("Expected 'text/xml' or 'application/xml' or 'text/html' or 'application/xhtml+xml' " +
"but tried to read from application/x-www-form-urlencoded encoded field 'age' which provides only text/plain values."))
}

"return an error when accessing a field of multipart forms for which no Unmarshaller is available" in {
marshal(multipartFormData)
.flatMap(_.as[HttpForm])
.flatMap(_.field("age").as[Int]) ===
Left(UnsupportedContentType("Field 'age' can only be read from 'application/x-www-form-urlencoded' form content"))
Left(UnsupportedContentType("Field 'age' can only be read from 'application/x-www-form-urlencoded' form content but was 'text/xml'"))
}
}

Expand Down
Expand Up @@ -20,15 +20,25 @@ import shapeless.HNil
import spray.httpx.marshalling.marshalUnsafe
import spray.httpx.unmarshalling.FromStringDeserializers.HexInt
import spray.http._
import scala.xml.NodeSeq
import spray.routing.directives.FieldDefMagnet

class FormFieldDirectivesSpec extends RoutingSpec {

val nodeSeq: xml.NodeSeq = <b>yes</b>
val urlEncodedForm = FormData(Map("firstName" -> "Mike", "age" -> "42"))
val urlEncodedFormWithVip = FormData(Map("firstName" -> "Mike", "age" -> "42", "VIP" -> "true"))
val multipartForm = MultipartFormData {
Map(
"firstName" -> BodyPart("Mike"),
"age" -> BodyPart(marshalUnsafe(<int>42</int>)))
"age" -> BodyPart(marshalUnsafe(<int>42</int>)),
"VIPBoolean" -> BodyPart("true"))
}
val multipartFormWithTextHtml = MultipartFormData {
Map(
"firstName" -> BodyPart("Mike"),
"age" -> BodyPart(marshalUnsafe(<int>42</int>)),
"VIP" -> BodyPart(HttpEntity(MediaTypes.`text/html`, "<b>yes</b>")))
}

"The 'formFields' extraction directive" should {
Expand Down Expand Up @@ -74,24 +84,36 @@ class FormFieldDirectivesSpec extends RoutingSpec {
}
} ~> check { rejection === MissingFormFieldRejection("sex") }
}
"create a proper error message if only a multipart unmarshaller is available for a www-urlencoded field" in {
Get("/", urlEncodedForm) ~> {
formFields('firstName, "age", 'sex?, "VIP" ? nodeSeq) { (firstName, age, sex, vip)
complete(firstName + age + sex + vip)
"properly extract the value if only a urlencoded deserializer is available for a multipart field that comes without a" +
"Content-Type (or text/plain)" in {
Get("/", multipartForm) ~> {
formFields('firstName, "age", 'sex?, "VIPBoolean" ? false) { (firstName, age, sex, vip)
complete(firstName + age + sex + vip)
}
} ~> check {
responseAs[String] === "Mike<int>42</int>Nonetrue"
}
} ~> check {
rejection === UnsupportedRequestContentTypeRejection("Field 'VIP' can only be read from " +
"'multipart/form-data' form content")
}
}
"create a proper error message if only a urlencoded deserializer is available for a multipart field" in {
Get("/", multipartForm) ~> {
formFields('firstName, "age", 'sex?, "VIP" ? false) { (firstName, age, sex, vip)
"create a proper error message if only a urlencoded deserializer is available for a multipart field with custom " +
"Content-Type" in {
Get("/", multipartFormWithTextHtml) ~> {
formFields(('firstName, "age", 'VIP ? false)) { (firstName, age, vip)
complete(firstName + age + vip)
}
} ~> check {
rejection === UnsupportedRequestContentTypeRejection("Field 'VIP' can only be read from " +
"'application/x-www-form-urlencoded' form content but was 'text/html'")
}
}
"create a proper error message if only a multipart unmarshaller is available for a www-urlencoded field" in {
Get("/", urlEncodedFormWithVip) ~> {
formFields('firstName, "age", 'sex?, "VIP" ? nodeSeq) { (firstName, age, sex, vip)
complete(firstName + age + sex + vip)
}
} ~> check {
rejection === UnsupportedRequestContentTypeRejection("Field 'VIP' can only be read from " +
"'application/x-www-form-urlencoded' form content")
rejection === UnsupportedRequestContentTypeRejection("Expected 'text/xml' or 'application/xml' or 'text/html' " +
"or 'application/xhtml+xml' but tried to read from application/x-www-form-urlencoded encoded " +
"field 'VIP' which provides only text/plain values.")
}
}
}
Expand Down

0 comments on commit ebaa580

Please sign in to comment.