diff --git a/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala b/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala index f405fba42f..896fc1c563 100644 --- a/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala +++ b/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala @@ -37,7 +37,7 @@ object HttpCharsetRange { final def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ "*;q=" ~~ qValue else r ~~ '*' def matches(charset: HttpCharset) = true def matchesAll: Boolean = true - def specimen = HttpCharsets.`UTF-8` + def specimen: HttpCharset = HttpCharsets.`UTF-8` def withQValue(qValue: Float) = if (qValue == 1.0f) `*` else if (qValue != this.qValue) `*`(qValue.toFloat) else this } @@ -47,7 +47,7 @@ object HttpCharsetRange { require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") def matches(charset: HttpCharset) = this.charset.value.equalsIgnoreCase(charset.value) def matchesAll: Boolean = false - def specimen = charset + def specimen: HttpCharset = charset def withQValue(qValue: Float) = One(charset, qValue) def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ charset ~~ ";q=" ~~ qValue else r ~~ charset } diff --git a/akka-http-tests/src/test/scala/akka/http/server/directives/FormFieldDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/server/directives/FormFieldDirectivesSpec.scala new file mode 100644 index 0000000000..71b875cf76 --- /dev/null +++ b/akka-http-tests/src/test/scala/akka/http/server/directives/FormFieldDirectivesSpec.scala @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.server +package directives + +import akka.http.common.StrictForm +import akka.http.marshallers.xml.ScalaXmlSupport +import akka.http.unmarshalling.Unmarshaller.HexInt +import akka.http.model._ +import MediaTypes._ + +class FormFieldDirectivesSpec extends RoutingSpec { + implicit val nodeSeqUnmarshaller = + ScalaXmlSupport.nodeSeqUnmarshaller(`text/xml`, `text/html`, `text/plain`) + + val nodeSeq: xml.NodeSeq = yes + val urlEncodedForm = FormData(Map("firstName" -> "Mike", "age" -> "42")) + val urlEncodedFormWithVip = FormData(Map("firstName" -> "Mike", "age" -> "42", "VIP" -> "true", "super" -> "no")) + val multipartForm = Multipart.FormData { + Map( + "firstName" -> HttpEntity("Mike"), + "age" -> HttpEntity(`text/xml`, "42"), + "VIPBoolean" -> HttpEntity("true")) + } + val multipartFormWithTextHtml = Multipart.FormData { + Map( + "firstName" -> HttpEntity("Mike"), + "age" -> HttpEntity(`text/xml`, "42"), + "VIP" -> HttpEntity(`text/html`, "yes"), + "super" -> HttpEntity("no")) + } + val multipartFormWithFile = Multipart.FormData( + Multipart.FormData.BodyPart.Strict("file", HttpEntity(MediaTypes.`text/xml`, "42"), + Map("filename" -> "age.xml"))) + + "The 'formFields' extraction directive" should { + "properly extract the value of www-urlencoded form fields" in { + Post("/", urlEncodedForm) ~> { + formFields('firstName, "age".as[Int], 'sex?, "VIP" ? false) { (firstName, age, sex, vip) ⇒ + complete(firstName + age + sex + vip) + } + } ~> check { responseAs[String] shouldEqual "Mike42Nonefalse" } + } + "properly extract the value of www-urlencoded form fields when an explicit unmarshaller is given" in { + Post("/", urlEncodedForm) ~> { + formFields('firstName, "age".as(HexInt), 'sex?, "VIP" ? false) { (firstName, age, sex, vip) ⇒ + complete(firstName + age + sex + vip) + } + } ~> check { responseAs[String] shouldEqual "Mike66Nonefalse" } + } + "properly extract the value of multipart form fields" in { + Post("/", multipartForm) ~> { + formFields('firstName, "age", 'sex?, "VIP" ? nodeSeq) { (firstName, age, sex, vip) ⇒ + complete(firstName + age + sex + vip) + } + } ~> check { responseAs[String] shouldEqual "Mike42Noneyes" } + } + "extract StrictForm.FileData from a multipart part" in { + Post("/", multipartFormWithFile) ~> { + formFields('file.as[StrictForm.FileData]) { + case StrictForm.FileData(name, HttpEntity.Strict(ct, data)) ⇒ + complete(s"type ${ct.mediaType} length ${data.length} filename ${name.get}") + } + } ~> check { responseAs[String] shouldEqual "type text/xml length 13 filename age.xml" } + } + "reject the request with a MissingFormFieldRejection if a required form field is missing" in { + Post("/", urlEncodedForm) ~> { + formFields('firstName, "age", 'sex, "VIP" ? false) { (firstName, age, sex, vip) ⇒ + complete(firstName + age + sex + vip) + } + } ~> check { rejection shouldEqual MissingFormFieldRejection("sex") } + } + "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 { + Post("/", multipartForm) ~> { + formFields('firstName, "age", 'sex?, "VIPBoolean" ? false) { (firstName, age, sex, vip) ⇒ + complete(firstName + age + sex + vip) + } + } ~> check { + responseAs[String] shouldEqual "Mike42Nonetrue" + } + } + "work even if only a FromStringUnmarshaller is available for a multipart field with custom Content-Type" in { + Post("/", multipartFormWithTextHtml) ~> { + formFields(('firstName, "age", 'super ? false)) { (firstName, age, vip) ⇒ + complete(firstName + age + vip) + } + } ~> check { + responseAs[String] shouldEqual "Mike42false" + } + } + "work even if only a FromEntityUnmarshaller is available for a www-urlencoded field" in { + Post("/", urlEncodedFormWithVip) ~> { + formFields('firstName, "age", 'sex?, "super" ? nodeSeq) { (firstName, age, sex, vip) ⇒ + complete(firstName + age + sex + vip) + } + } ~> check { + responseAs[String] shouldEqual "Mike42Noneno" + } + } + } + "The 'formField' requirement directive" should { + "block requests that do not contain the required formField" in { + Post("/", urlEncodedForm) ~> { + formFields('name ! "Mr. Mike") { completeOk } + } ~> check { handled shouldEqual false } + } + "block requests that contain the required parameter but with an unmatching value" in { + Post("/", urlEncodedForm) ~> { + formFields('firstName ! "Pete") { completeOk } + } ~> check { handled shouldEqual false } + } + "let requests pass that contain the required parameter with its required value" in { + Post("/", urlEncodedForm) ~> { + formFields('firstName ! "Mike") { completeOk } + } ~> check { response shouldEqual Ok } + } + } + + "The 'formField' requirement with explicit unmarshaller directive" should { + "block requests that do not contain the required formField" in { + Post("/", urlEncodedForm) ~> { + formFields('oldAge.as(HexInt) ! 78) { completeOk } + } ~> check { handled shouldEqual false } + } + "block requests that contain the required parameter but with an unmatching value" in { + Post("/", urlEncodedForm) ~> { + formFields('age.as(HexInt) ! 78) { completeOk } + } ~> check { handled shouldEqual false } + } + "let requests pass that contain the required parameter with its required value" in { + Post("/", urlEncodedForm) ~> { + formFields('age.as(HexInt) ! 66 /* hex! */ ) { completeOk } + } ~> check { response shouldEqual Ok } + } + } + +} diff --git a/akka-http/src/main/scala/akka/http/common/NameReceptacle.scala b/akka-http/src/main/scala/akka/http/common/NameReceptacle.scala new file mode 100644 index 0000000000..7005405536 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/common/NameReceptacle.scala @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.common + +import akka.http.unmarshalling.{ FromStringUnmarshaller ⇒ FSU } + +private[http] trait ToNameReceptacleEnhancements { + implicit def symbol2NR(symbol: Symbol) = new NameReceptacle[String](symbol.name) + implicit def string2NR(string: String) = new NameReceptacle[String](string) +} + +class NameReceptacle[T](val name: String) { + def as[B] = new NameReceptacle[B](name) + def as[B](unmarshaller: FSU[B]) = new NameUnmarshallerReceptacle(name, unmarshaller) + def ? = new NameOptionReceptacle[T](name) + def ?[B](default: B) = new NameDefaultReceptacle(name, default) + def ![B](requiredValue: B) = new RequiredValueReceptacle(name, requiredValue) +} + +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) +} + +class NameOptionReceptacle[T](val name: String) + +class NameDefaultReceptacle[T](val name: String, val default: T) + +class RequiredValueReceptacle[T](val name: String, val requiredValue: T) + +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 diff --git a/akka-http/src/main/scala/akka/http/common/StrictForm.scala b/akka-http/src/main/scala/akka/http/common/StrictForm.scala new file mode 100644 index 0000000000..15842162e7 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/common/StrictForm.scala @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.common + +import scala.annotation.implicitNotFound +import scala.collection.immutable +import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.duration._ +import akka.stream.FlowMaterializer +import akka.http.util.FastFuture +import akka.http.unmarshalling._ +import akka.http.model._ +import FastFuture._ + +/** + * Read-only abstraction on top of `application/x-www-form-urlencoded` and multipart form data, + * allowing joint unmarshalling access to either kind, **if** you supply both, a [[FromStringUnmarshaller]] + * as well as a [[FromEntityUnmarshaller]] for the target type `T`. + * Note: In order to allow for random access to the field values streamed multipart form data are strictified! + * Don't use this abstraction on potentially unbounded forms (e.g. large file uploads). + * + * If you only need to consume one type of form (`application/x-www-form-urlencoded` *or* multipart) then + * simply unmarshal directly to the respective form abstraction ([[FormData]] or [[Multipart.FormData]]) + * rather than going through [[StrictForm]]. + * + * Simple usage example: + * {{{ + * val strictFormFuture = Unmarshal(entity).to[StrictForm] + * val fooFieldUnmarshalled: Future[T] = + * strictFormFuture flatMap { form => + * Unmarshal(form field "foo").to[T] + * } + * }}} + */ +sealed abstract class StrictForm { + def fields: immutable.Seq[(String, StrictForm.Field)] + def field(name: String): Option[StrictForm.Field] = fields collectFirst { case (`name`, field) ⇒ field } +} + +object StrictForm { + sealed trait Field + object Field { + private[StrictForm] final case class FromString(value: String) extends Field + private[StrictForm] final case class FromPart(value: Multipart.FormData.BodyPart.Strict) extends Field + + implicit def unmarshaller[T](implicit um: FieldUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] = + Unmarshaller { + case FromString(value) ⇒ um.unmarshalString(value) + case FromPart(value) ⇒ um.unmarshalPart(value) + } + + def unmarshallerFromFSU[T](fsu: FromStringUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] = + Unmarshaller { + case FromString(value) ⇒ fsu(value) + case FromPart(value) ⇒ fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name)) + } + + @implicitNotFound("In order to unmarshal a `StrictForm.Field` to type `${T}` you need to supply a " + + "`FromStringUnmarshaller[${T}]` and/or a `FromEntityUnmarshaller[${T}]`") + sealed trait FieldUnmarshaller[T] { + def unmarshalString(value: String): Future[T] + def unmarshalPart(value: Multipart.FormData.BodyPart.Strict): Future[T] + } + object FieldUnmarshaller extends LowPrioImplicits { + implicit def fromBoth[T](implicit fsu: FromStringUnmarshaller[T], feu: FromEntityUnmarshaller[T]) = + new FieldUnmarshaller[T] { + def unmarshalString(value: String) = fsu(value) + def unmarshalPart(value: Multipart.FormData.BodyPart.Strict) = feu(value.entity) + } + } + sealed abstract class LowPrioImplicits { + implicit def fromFSU[T](implicit fsu: FromStringUnmarshaller[T]) = + new FieldUnmarshaller[T] { + def unmarshalString(value: String) = fsu(value) + def unmarshalPart(value: Multipart.FormData.BodyPart.Strict) = + fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name)) + } + implicit def fromFEU[T](implicit feu: FromEntityUnmarshaller[T]) = + new FieldUnmarshaller[T] { + def unmarshalString(value: String) = feu(HttpEntity(value)) + def unmarshalPart(value: Multipart.FormData.BodyPart.Strict) = feu(value.entity) + } + } + } + + implicit def unmarshaller(implicit formDataUM: FromEntityUnmarshaller[FormData], + multipartUM: FromEntityUnmarshaller[Multipart.FormData], + ec: ExecutionContext, fm: FlowMaterializer): FromEntityUnmarshaller[StrictForm] = { + + def tryUnmarshalToQueryForm(entity: HttpEntity): Future[StrictForm] = + for (formData ← formDataUM(entity).fast) yield { + new StrictForm { + val fields = formData.fields.map { case (name, value) ⇒ name -> Field.FromString(value) }(collection.breakOut) + } + } + + def tryUnmarshalToMultipartForm(entity: HttpEntity): Future[StrictForm] = + for { + multiPartFD ← multipartUM(entity).fast + strictMultiPartFD ← multiPartFD.toStrict(10.seconds).fast // TODO: make timeout configurable + } yield { + new StrictForm { + val fields = strictMultiPartFD.strictParts.map { + case x: Multipart.FormData.BodyPart.Strict ⇒ x.name -> Field.FromPart(x) + }(collection.breakOut) + } + } + + Unmarshaller { entity ⇒ + tryUnmarshalToQueryForm(entity).fast.recoverWith { + case Unmarshaller.UnsupportedContentTypeException(supported1) ⇒ + tryUnmarshalToMultipartForm(entity).fast.recoverWith { + case Unmarshaller.UnsupportedContentTypeException(supported2) ⇒ + FastFuture.failed(Unmarshaller.UnsupportedContentTypeException(supported1 ++ supported2)) + } + } + } + } + + /** + * Simple model for strict file content in a multipart form data part. + */ + final case class FileData(filename: Option[String], entity: HttpEntity.Strict) + + object FileData { + implicit val unmarshaller: FromStrictFormFieldUnmarshaller[FileData] = + Unmarshaller strict { + case Field.FromString(_) ⇒ throw Unmarshaller.UnsupportedContentTypeException(MediaTypes.`application/x-www-form-urlencoded`) + case Field.FromPart(part) ⇒ FileData(part.filename, part.entity) + } + } +} diff --git a/akka-http/src/main/scala/akka/http/server/Directives.scala b/akka-http/src/main/scala/akka/http/server/Directives.scala index 29a0e84302..5b877d6025 100644 --- a/akka-http/src/main/scala/akka/http/server/Directives.scala +++ b/akka-http/src/main/scala/akka/http/server/Directives.scala @@ -18,7 +18,7 @@ trait Directives extends RouteConcatenation with CodingDirectives with ExecutionDirectives with FileAndResourceDirectives - //with FormFieldDirectives + with FormFieldDirectives with FutureDirectives with HeaderDirectives with HostDirectives diff --git a/akka-http/src/main/scala/akka/http/server/PathMatcher.scala b/akka-http/src/main/scala/akka/http/server/PathMatcher.scala index 5807c92d45..986b6437f6 100644 --- a/akka-http/src/main/scala/akka/http/server/PathMatcher.scala +++ b/akka-http/src/main/scala/akka/http/server/PathMatcher.scala @@ -9,9 +9,9 @@ import scala.util.matching.Regex import scala.annotation.tailrec import akka.http.server.util.Tuple import akka.http.server.util.TupleOps._ +import akka.http.common.NameOptionReceptacle import akka.http.model.Uri.Path import akka.http.util._ -import directives.NameReceptacle /** * A PathMatcher tries to match a prefix of a given string and returns either a PathMatcher.Matched instance @@ -216,7 +216,7 @@ trait ImplicitPathMatcherConstruction { implicit def segmentStringToPathMatcher(segment: String): PathMatcher0 = PathMatcher(segment :: Path.Empty, ()) - implicit def stringOptionNameReceptacle2PathMatcher(nr: NameReceptacle[Option[String]]): PathMatcher0 = + implicit def stringNameOptionReceptacle2PathMatcher(nr: NameOptionReceptacle[String]): PathMatcher0 = PathMatcher(nr.name).? /** diff --git a/akka-http/src/main/scala/akka/http/server/directives/FormFieldDirectives.scala b/akka-http/src/main/scala/akka/http/server/directives/FormFieldDirectives.scala new file mode 100644 index 0000000000..52df830ba1 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/server/directives/FormFieldDirectives.scala @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.server +package directives + +import scala.concurrent.{ Future, ExecutionContext } +import scala.util.{ Failure, Success } +import akka.http.unmarshalling.Unmarshaller.UnsupportedContentTypeException +import akka.http.common._ +import akka.http.util._ +import FastFuture._ + +trait FormFieldDirectives extends ToNameReceptacleEnhancements { + import FormFieldDirectives._ + + /** + * Rejects the request if the defined form field matcher(s) don't match. + * Otherwise the form field value(s) are extracted and passed to the inner route. + */ + def formField(pdm: FieldMagnet): pdm.Out = pdm() + + /** + * Rejects the request if the defined form field matcher(s) don't match. + * Otherwise the form field value(s) are extracted and passed to the inner route. + */ + def formFields(pdm: FieldMagnet): pdm.Out = pdm() + +} + +object FormFieldDirectives extends FormFieldDirectives { + sealed trait FieldMagnet { + type Out + def apply(): Out + } + object FieldMagnet { + implicit def apply[T](value: T)(implicit fdef: FieldDef[T]) = + new FieldMagnet { + type Out = fdef.Out + def apply() = fdef(value) + } + } + + sealed trait FieldDef[T] { + type Out + def apply(value: T): Out + } + + object FieldDef { + def fieldDef[A, B](f: A ⇒ B) = + new FieldDef[A] { + type Out = B + def apply(value: A) = f(value) + } + + import akka.http.unmarshalling.{ FromStrictFormFieldUnmarshaller ⇒ FSFFU, _ } + import BasicDirectives._ + import RouteDirectives._ + import FutureDirectives._ + type SFU = FromEntityUnmarshaller[StrictForm] + type FSFFOU[T] = Unmarshaller[Option[StrictForm.Field], T] + + //////////////////// "regular" formField extraction //////////////////// + + private def extractField[A, B](f: 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))) + } + } + } + implicit def forString(implicit sfu: SFU, fu: FSFFU[String]) = + extractField[String, String] { fieldName ⇒ filter(fieldName, fu) } + implicit def forSymbol(implicit sfu: SFU, fu: FSFFU[String]) = + extractField[Symbol, String] { symbol ⇒ filter(symbol.name, fu) } + implicit def forNR[T](implicit sfu: SFU, fu: FSFFU[T]) = + extractField[NameReceptacle[T], T] { nr ⇒ filter(nr.name, fu) } + implicit def forNUR[T](implicit sfu: SFU) = + extractField[NameUnmarshallerReceptacle[T], T] { nr ⇒ filter(nr.name, StrictForm.Field.unmarshallerFromFSU(nr.um)) } + implicit def forNOR[T](implicit sfu: SFU, fu: FSFFOU[T], ec: ExecutionContext) = + extractField[NameOptionReceptacle[T], Option[T]] { nr ⇒ filter[Option[T]](nr.name, fu) } + implicit def forNDR[T](implicit sfu: SFU, fu: FSFFOU[T], ec: ExecutionContext) = + extractField[NameDefaultReceptacle[T], T] { nr ⇒ filter(nr.name, fu withDefaultValue nr.default) } + implicit def forNOUR[T](implicit sfu: SFU, ec: ExecutionContext) = + extractField[NameOptionUnmarshallerReceptacle[T], Option[T]] { nr ⇒ filter[Option[T]](nr.name, StrictForm.Field.unmarshallerFromFSU(nr.um): FSFFOU[T]) } + implicit def forNDUR[T](implicit sfu: SFU, ec: ExecutionContext) = + extractField[NameDefaultUnmarshallerReceptacle[T], T] { nr ⇒ filter(nr.name, (StrictForm.Field.unmarshallerFromFSU(nr.um): FSFFOU[T]) withDefaultValue nr.default) } + + //////////////////// required formField support //////////////////// + + private def requiredFilter[T](fieldName: String, fu: Unmarshaller[Option[StrictForm.Field], T], + requiredValue: Any)(implicit sfu: SFU): Directive0 = + extract(fieldOfForm(fieldName, fu)).flatMap { + onComplete(_).flatMap { + case Success(value) if value == requiredValue ⇒ pass + case _ ⇒ reject + } + } + implicit def forRVR[T](implicit sfu: SFU, fu: FSFFU[T]) = + fieldDef[RequiredValueReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, fu, rvr.requiredValue) } + implicit def forRVDR[T](implicit sfu: SFU) = + fieldDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, StrictForm.Field.unmarshallerFromFSU(rvr.um), rvr.requiredValue) } + + //////////////////// tuple support //////////////////// + + import akka.http.server.util.TupleOps._ + import akka.http.server.util.BinaryPolyFunc + + implicit def forTuple[T](implicit fold: FoldLeft[Directive0, T, ConvertParamDefAndConcatenate.type]) = + fieldDef[T, fold.Out](fold(pass, _)) + + object ConvertParamDefAndConcatenate extends BinaryPolyFunc { + implicit def from[P, TA, TB](implicit fdef: FieldDef[P] { type Out = Directive[TB] }, ev: Join[TA, TB]) = + at[Directive[TA], P] { (a, t) ⇒ a & fdef(t) } + } + } +} diff --git a/akka-http/src/main/scala/akka/http/server/directives/MethodDirectives.scala b/akka-http/src/main/scala/akka/http/server/directives/MethodDirectives.scala index 72fc4b9482..ca5b050e24 100644 --- a/akka-http/src/main/scala/akka/http/server/directives/MethodDirectives.scala +++ b/akka-http/src/main/scala/akka/http/server/directives/MethodDirectives.scala @@ -12,41 +12,42 @@ trait MethodDirectives { import BasicDirectives._ import RouteDirectives._ import ParameterDirectives._ + import MethodDirectives._ /** * A route filter that rejects all non-DELETE requests. */ - def delete: Directive0 = MethodDirectives._delete + def delete: Directive0 = _delete /** * A route filter that rejects all non-GET requests. */ - def get: Directive0 = MethodDirectives._get + def get: Directive0 = _get /** * A route filter that rejects all non-HEAD requests. */ - def head: Directive0 = MethodDirectives._head + def head: Directive0 = _head /** * A route filter that rejects all non-OPTIONS requests. */ - def options: Directive0 = MethodDirectives._options + def options: Directive0 = _options /** * A route filter that rejects all non-PATCH requests. */ - def patch: Directive0 = MethodDirectives._patch + def patch: Directive0 = _patch /** * A route filter that rejects all non-POST requests. */ - def post: Directive0 = MethodDirectives._post + def post: Directive0 = _post /** * A route filter that rejects all non-PUT requests. */ - def put: Directive0 = MethodDirectives._put + def put: Directive0 = _put /** * Rejects all requests whose HTTP method does not match the given one. @@ -67,13 +68,15 @@ trait MethodDirectives { * - Supporting older browsers that lack support for certain HTTP methods. E.g. IE8 does not support PATCH */ def overrideMethodWithParameter(paramName: String): Directive0 = - parameter(paramName?) flatMap { - case Some(method) ⇒ - getForKey(method.toUpperCase) match { - case Some(m) ⇒ mapRequest(_.copy(method = m)) - case _ ⇒ complete(StatusCodes.NotImplemented) - } - case _ ⇒ pass + extractExecutionContext flatMap { implicit ec ⇒ + parameter(paramName?) flatMap { + case Some(method) ⇒ + getForKey(method.toUpperCase) match { + case Some(m) ⇒ mapRequest(_.copy(method = m)) + case _ ⇒ complete(StatusCodes.NotImplemented) + } + case None ⇒ pass + } } } diff --git a/akka-http/src/main/scala/akka/http/server/directives/NameReceptacle.scala b/akka-http/src/main/scala/akka/http/server/directives/NameReceptacle.scala deleted file mode 100644 index 6eb26267c9..0000000000 --- a/akka-http/src/main/scala/akka/http/server/directives/NameReceptacle.scala +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2009-2014 Typesafe Inc. - */ - -package akka.http.server -package directives - -import scala.concurrent.ExecutionContext -import akka.http.unmarshalling.{ FromStringOptionUnmarshaller ⇒ FSOU, Unmarshaller } - -trait ToNameReceptacleEnhancements { - implicit def symbol2NR(symbol: Symbol) = new NameReceptacle[String](symbol.name) - implicit def string2NR(string: String) = new NameReceptacle[String](string) -} - -case class NameReceptacle[A](name: String) { - def as[B] = NameReceptacle[B](name) - def as[B](unmarshaller: FSOU[B]) = NameUnmarshallerReceptacle(name, _ ⇒ unmarshaller) - def ? = as[Option[A]] - def ?[B](default: B) = NameDefaultReceptacle(name, default) - def ![B](requiredValue: B) = RequiredValueReceptacle(name, requiredValue) -} - -case class NameUnmarshallerReceptacle[A](name: String, um: ExecutionContext ⇒ FSOU[A]) { - def ? = NameUnmarshallerReceptacle(name, ec ⇒ Unmarshaller.targetOptionUnmarshaller(um(ec), ec)) - def ?(default: A) = NameUnmarshallerDefaultReceptacle(name, um, default) - def !(requiredValue: A) = RequiredValueUnmarshallerReceptacle(name, um, requiredValue) -} - -case class NameDefaultReceptacle[A](name: String, default: A) - -case class RequiredValueReceptacle[A](name: String, requiredValue: A) - -case class NameUnmarshallerDefaultReceptacle[A](name: String, um: ExecutionContext ⇒ FSOU[A], default: A) - -case class RequiredValueUnmarshallerReceptacle[A](name: String, um: ExecutionContext ⇒ FSOU[A], requiredValue: A) \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/server/directives/ParameterDirectives.scala b/akka-http/src/main/scala/akka/http/server/directives/ParameterDirectives.scala index 1829267a09..f2381fd416 100644 --- a/akka-http/src/main/scala/akka/http/server/directives/ParameterDirectives.scala +++ b/akka-http/src/main/scala/akka/http/server/directives/ParameterDirectives.scala @@ -8,33 +8,35 @@ package directives import scala.collection.immutable import scala.concurrent.ExecutionContext import scala.util.{ Failure, Success } +import akka.http.common._ import akka.http.util._ trait ParameterDirectives extends ToNameReceptacleEnhancements { + import ParameterDirectives._ /** * Extracts the requests query parameters as a Map[String, String]. */ - def parameterMap: Directive1[Map[String, String]] = ParameterDirectives._parameterMap + def parameterMap: Directive1[Map[String, String]] = _parameterMap /** * Extracts the requests query parameters as a Map[String, List[String]]. */ - def parameterMultiMap: Directive1[Map[String, List[String]]] = ParameterDirectives._parameterMultiMap + def parameterMultiMap: Directive1[Map[String, List[String]]] = _parameterMultiMap /** * Extracts the requests query parameters as a Seq[(String, String)]. */ - def parameterSeq: Directive1[immutable.Seq[(String, String)]] = ParameterDirectives._parameterSeq + def parameterSeq: Directive1[immutable.Seq[(String, String)]] = _parameterSeq /** - * Rejects the request if the query parameter matcher(s) defined by the definition(s) don't match. + * Rejects the request if the defined query parameter matcher(s) don't match. * Otherwise the parameter value(s) are extracted and passed to the inner route. */ def parameter(pdm: ParamMagnet): pdm.Out = pdm() /** - * Rejects the request if the query parameter matcher(s) defined by the definition(s) don't match. + * Rejects the request if the defined query parameter matcher(s) don't match. * Otherwise the parameter value(s) are extracted and passed to the inner route. */ def parameters(pdm: ParamMagnet): pdm.Out = pdm() @@ -52,86 +54,89 @@ object ParameterDirectives extends ParameterDirectives { private val _parameterSeq: Directive1[immutable.Seq[(String, String)]] = extract(_.request.uri.query.toSeq) -} -trait ParamMagnet { - type Out - def apply(): Out -} -object ParamMagnet { - implicit def apply[T](value: T)(implicit pdef: ParamDef[T]) = - new ParamMagnet { - type Out = pdef.Out - def apply() = pdef(value) - } -} - -trait ParamDef[T] { - type Out - def apply(value: T): Out -} - -object ParamDef { - def paramDef[A, B](f: A ⇒ B) = - new ParamDef[A] { - type Out = B - def apply(value: A) = f(value) - } - - import akka.http.unmarshalling.{ FromStringOptionUnmarshaller ⇒ FSOU, _ } - import BasicDirectives._ - import RouteDirectives._ - import FutureDirectives._ - import UnmarshallingError._ - - /************ "regular" parameter extraction ******************/ - - private def extractParameter[A, B](f: A ⇒ Directive1[B]) = paramDef(f) - private def filter[T](paramName: String, fsou: ExecutionContext ⇒ FSOU[T]): Directive1[T] = - extract(ctx ⇒ fsou(ctx.executionContext)(ctx.request.uri.query get paramName)).flatMap { - onComplete(_).flatMap { - case Success(x) ⇒ provide(x) - case Failure(ContentExpected) ⇒ reject(MissingQueryParamRejection(paramName)) - case Failure(x) ⇒ reject(MalformedQueryParamRejection(paramName, x.getMessage.nullAsEmpty, Option(x.getCause))) - } - } - implicit def forString(implicit fsou: FSOU[String]) = - extractParameter[String, String] { string ⇒ filter(string, _ ⇒ fsou) } - implicit def forSymbol(implicit fsou: FSOU[String]) = - extractParameter[Symbol, String] { symbol ⇒ filter(symbol.name, _ ⇒ fsou) } - implicit def forNUmR[T] = - extractParameter[NameUnmarshallerReceptacle[T], T] { nr ⇒ filter(nr.name, nr.um) } - implicit def forNDefR[T](implicit fsou: FSOU[T]) = - extractParameter[NameDefaultReceptacle[T], T] { nr ⇒ filter(nr.name, ec ⇒ fsou.withDefaultValue(nr.default)(ec)) } - implicit def forNUmDefR[T] = - extractParameter[NameUnmarshallerDefaultReceptacle[T], T] { nr ⇒ filter(nr.name, ec ⇒ nr.um(ec).withDefaultValue(nr.default)(ec)) } - implicit def forNR[T](implicit fsou: FSOU[T]) = - extractParameter[NameReceptacle[T], T] { nr ⇒ filter(nr.name, _ ⇒ fsou) } - - /************ required parameter support ******************/ - - private def requiredFilter(paramName: String, fsou: ExecutionContext ⇒ FSOU[_], requiredValue: Any): Directive0 = - extract(ctx ⇒ fsou(ctx.executionContext)(ctx.request.uri.query.get(paramName))).flatMap { deserialisationFuture ⇒ - onComplete(deserialisationFuture).flatMap { - case Success(value) if value == requiredValue ⇒ pass - case _ ⇒ reject - } - } - implicit def forRVR[T](implicit fsou: FSOU[T]) = - paramDef[RequiredValueReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, _ ⇒ fsou, rvr.requiredValue) } - implicit def forRVDR[T] = - paramDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, rvr.um, rvr.requiredValue) } - - /************ tuple support ******************/ - - import akka.http.server.util.TupleOps._ - import akka.http.server.util.BinaryPolyFunc - - implicit def forTuple[T](implicit fold: FoldLeft[Directive0, T, ConvertParamDefAndConcatenate.type]) = - paramDef[T, fold.Out](fold(BasicDirectives.pass, _)) - - object ConvertParamDefAndConcatenate extends BinaryPolyFunc { - implicit def from[P, TA, TB](implicit pdef: ParamDef[P] { type Out = Directive[TB] }, ev: Join[TA, TB]) = - at[Directive[TA], P] { (a, t) ⇒ a & pdef(t) } + sealed trait ParamMagnet { + type Out + def apply(): Out } -} + object ParamMagnet { + implicit def apply[T](value: T)(implicit pdef: ParamDef[T]) = + new ParamMagnet { + type Out = pdef.Out + def apply() = pdef(value) + } + } + + sealed trait ParamDef[T] { + type Out + def apply(value: T): Out + } + object ParamDef { + def paramDef[A, B](f: A ⇒ B) = + new ParamDef[A] { + type Out = B + def apply(value: A) = f(value) + } + + import akka.http.unmarshalling.{ FromStringUnmarshaller ⇒ FSU, _ } + import BasicDirectives._ + import RouteDirectives._ + import FutureDirectives._ + type FSOU[T] = Unmarshaller[Option[String], T] + + //////////////////// "regular" parameter extraction ////////////////////// + + private def extractParameter[A, B](f: A ⇒ Directive1[B]) = paramDef(f) + private def filter[T](paramName: String, fsou: FSOU[T]): Directive1[T] = + extractUri flatMap { uri ⇒ + onComplete(fsou(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))) + } + } + implicit def forString(implicit fsu: FSU[String]) = + extractParameter[String, String] { string ⇒ filter(string, fsu) } + implicit def forSymbol(implicit fsu: FSU[String]) = + extractParameter[Symbol, String] { symbol ⇒ filter(symbol.name, fsu) } + implicit def forNR[T](implicit fsu: FSU[T]) = + extractParameter[NameReceptacle[T], T] { nr ⇒ filter(nr.name, fsu) } + implicit def forNUR[T] = + extractParameter[NameUnmarshallerReceptacle[T], T] { nr ⇒ filter(nr.name, nr.um) } + implicit def forNOR[T](implicit fsou: FSOU[T], ec: ExecutionContext) = + extractParameter[NameOptionReceptacle[T], Option[T]] { nr ⇒ filter[Option[T]](nr.name, fsou) } + implicit def forNDR[T](implicit fsou: FSOU[T], ec: ExecutionContext) = + extractParameter[NameDefaultReceptacle[T], T] { nr ⇒ filter[T](nr.name, fsou withDefaultValue nr.default) } + implicit def forNOUR[T](implicit ec: ExecutionContext) = + extractParameter[NameOptionUnmarshallerReceptacle[T], Option[T]] { nr ⇒ filter(nr.name, nr.um: FSOU[T]) } + implicit def forNDUR[T](implicit ec: ExecutionContext) = + extractParameter[NameDefaultUnmarshallerReceptacle[T], T] { nr ⇒ filter[T](nr.name, (nr.um: FSOU[T]) withDefaultValue nr.default) } + + //////////////////// required parameter support //////////////////// + + private def requiredFilter[T](paramName: String, fsou: FSOU[T], requiredValue: Any): Directive0 = + extractUri flatMap { uri ⇒ + onComplete(fsou(uri.query get paramName)) flatMap { + case Success(value) if value == requiredValue ⇒ pass + case _ ⇒ reject + } + } + implicit def forRVR[T](implicit fsu: FSU[T]) = + paramDef[RequiredValueReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, fsu, rvr.requiredValue) } + implicit def forRVDR[T] = + paramDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, rvr.um, rvr.requiredValue) } + + //////////////////// tuple support //////////////////// + + import akka.http.server.util.TupleOps._ + import akka.http.server.util.BinaryPolyFunc + + implicit def forTuple[T](implicit fold: FoldLeft[Directive0, T, ConvertParamDefAndConcatenate.type]) = + paramDef[T, fold.Out](fold(BasicDirectives.pass, _)) + + object ConvertParamDefAndConcatenate extends BinaryPolyFunc { + implicit def from[P, TA, TB](implicit pdef: ParamDef[P] { type Out = Directive[TB] }, ev: Join[TA, TB]) = + at[Directive[TA], P] { (a, t) ⇒ a & pdef(t) } + } + } +} \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/server/directives/PathDirectives.scala b/akka-http/src/main/scala/akka/http/server/directives/PathDirectives.scala index d60b8cff71..d684cada44 100644 --- a/akka-http/src/main/scala/akka/http/server/directives/PathDirectives.scala +++ b/akka-http/src/main/scala/akka/http/server/directives/PathDirectives.scala @@ -5,6 +5,8 @@ package akka.http.server package directives +import akka.http.common.ToNameReceptacleEnhancements + trait PathDirectives extends PathMatchers with ImplicitPathMatcherConstruction with ToNameReceptacleEnhancements { import BasicDirectives._ import RouteDirectives._ diff --git a/akka-http/src/main/scala/akka/http/unmarshalling/PredefinedFromStringUnmarshallers.scala b/akka-http/src/main/scala/akka/http/unmarshalling/PredefinedFromStringUnmarshallers.scala index e826885b3a..75155d2b39 100644 --- a/akka-http/src/main/scala/akka/http/unmarshalling/PredefinedFromStringUnmarshallers.scala +++ b/akka-http/src/main/scala/akka/http/unmarshalling/PredefinedFromStringUnmarshallers.scala @@ -61,13 +61,13 @@ trait PredefinedFromStringUnmarshallers { case "true" | "yes" | "on" ⇒ true case "false" | "no" | "off" ⇒ false case "" ⇒ throw Unmarshaller.NoContentException - case x ⇒ sys.error(s"'$x' is not a valid Boolean value") + case x ⇒ throw new IllegalArgumentException(s"'$x' is not a valid Boolean value") } } private def numberFormatError(value: String, target: String): PartialFunction[Throwable, Nothing] = { case e: NumberFormatException ⇒ - throw if (value.isEmpty) Unmarshaller.NoContentException else new RuntimeException(s"'$value' is not a valid $target value", e) + throw if (value.isEmpty) Unmarshaller.NoContentException else new IllegalArgumentException(s"'$value' is not a valid $target value", e) } } diff --git a/akka-http/src/main/scala/akka/http/unmarshalling/package.scala b/akka-http/src/main/scala/akka/http/unmarshalling/package.scala index 68f2a65490..50fe623da0 100644 --- a/akka-http/src/main/scala/akka/http/unmarshalling/package.scala +++ b/akka-http/src/main/scala/akka/http/unmarshalling/package.scala @@ -4,6 +4,7 @@ package akka.http +import akka.http.common.StrictForm import akka.http.model._ package object unmarshalling { @@ -12,5 +13,5 @@ package object unmarshalling { type FromResponseUnmarshaller[T] = Unmarshaller[HttpResponse, T] type FromRequestUnmarshaller[T] = Unmarshaller[HttpRequest, T] type FromStringUnmarshaller[T] = Unmarshaller[String, T] - type FromStringOptionUnmarshaller[T] = Unmarshaller[Option[String], T] + type FromStrictFormFieldUnmarshaller[T] = Unmarshaller[StrictForm.Field, T] }