From bd44a1d2d0e6152926c89534742ddb73e71b960d Mon Sep 17 00:00:00 2001 From: Omar Ahmed Date: Sun, 13 Jan 2019 02:01:59 -0500 Subject: [PATCH 01/11] Add HttpMethodOverrider middleware --- .../middleware/HttpMethodOverrider.scala | 87 ++++++++ .../middleware/HttpMethodOverriderSpec.scala | 204 ++++++++++++++++++ .../org/http4s/testing/Http4sMatchers.scala | 5 + 3 files changed, 296 insertions(+) create mode 100644 server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala create mode 100644 server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala diff --git a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala new file mode 100644 index 00000000000..bf3a9d89933 --- /dev/null +++ b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala @@ -0,0 +1,87 @@ +package org.http4s.server.middleware + +import cats.Applicative +import cats.data.Kleisli +import cats.implicits._ +import org.http4s.util.CaseInsensitiveString +import org.http4s.{AttributeKey, Header, HttpApp, Method, ParseResult, Request, Response, Status} + +object HttpMethodOverrider { + + /** + * HttpMethodOverrider middleware config options. + */ + final case class HttpMethodOverriderConfig( + overrideStrategy: OverrideStrategy, + overridableMethods: List[Method]) + + sealed trait OverrideStrategy + final case class HeaderOverrideStrategy(headerName: CaseInsensitiveString) + extends OverrideStrategy + final case class QueryOverrideStrategy(paramName: String) extends OverrideStrategy + + val defaultConfig = HttpMethodOverriderConfig( + HeaderOverrideStrategy(CaseInsensitiveString("X-HTTP-Method-Override")), + List(Method.POST)) + + val overriddenMethodAttrKey: AttributeKey[Method] = AttributeKey[Method] + + /** Simple middleware for HTTP Method Override. + * + * This middleware lets you use HTTP verbs such as PUT or DELETE in places where the client + * doesn't support it. Camouflage your request with another HTTP verb(usually POST) and sneak + * the desired one using a custom header or request parameter. The middleware will '''override''' + * the original verb with the new one for you, allowing the request the be dispatched properly. + * + * @param http [[HttpApp]] to transform + * @param config http method overrider config + */ + def apply[F[_]](http: HttpApp[F], config: HttpMethodOverriderConfig)( + implicit F: Applicative[F]): HttpApp[F] = { + + def processRequestWithMethod( + req: Request[F], + parseResult: ParseResult[Method]): F[Response[F]] = parseResult match { + case Left(_) => F.pure(Response[F](Status.MethodNotAllowed)) + case Right(om) => http(updateRequestWithMethod(req, om)).map(updateVaryHeader) + } + + def updateVaryHeader(resp: Response[F]): Response[F] = { + val varyHeaderName = CaseInsensitiveString("Vary") + config.overrideStrategy match { + case HeaderOverrideStrategy(headerName) => + val updatedVaryHeader = + resp.headers + .get(varyHeaderName) + .map((h: Header) => Header(h.name.value, s"${h.value}, ${headerName.value}")) + .getOrElse(Header(varyHeaderName.value, headerName.value)) + + resp.withHeaders(resp.headers.put(updatedVaryHeader)) + case QueryOverrideStrategy(_) => resp + } + } + + def updateRequestWithMethod(req: Request[F], om: Method): Request[F] = { + val attrs = req.attributes ++ Seq(overriddenMethodAttrKey(req.method)) + req.withAttributes(attrs).withMethod(om) + } + + def ignoresOverrideIfNotAllowed(req: Request[F]): Option[Unit] = + if (config.overridableMethods.contains(req.method)) Some(()) else None + + def getUnsafeOverrideMethod(req: Request[F]): Option[String] = + config.overrideStrategy match { + case HeaderOverrideStrategy(headerName) => req.headers.get(headerName).map(_.value) + case QueryOverrideStrategy(parameter) => req.params.get(parameter) + } + + Kleisli { req: Request[F] => + { + (ignoresOverrideIfNotAllowed(req) *> getUnsafeOverrideMethod(req)) + .map(m => Method.fromString(m.toUpperCase)) + .map(processRequestWithMethod(req, _)) + .getOrElse(http(req)) + } + } + } +} diff --git a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala new file mode 100644 index 00000000000..a1a2c6b246c --- /dev/null +++ b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala @@ -0,0 +1,204 @@ +package org.http4s.server.middleware + +import cats.effect.IO +import org.http4s._ +import org.http4s.dsl.io._ +import org.http4s.server.Router +import org.http4s.server.middleware.HttpMethodOverrider.{ + HeaderOverrideStrategy, + HttpMethodOverriderConfig, + QueryOverrideStrategy, + defaultConfig +} +import org.http4s.util.CaseInsensitiveString + +class HttpMethodOverriderSpec extends Http4sSpec { + + private final val overrideHeader = "X-HTTP-Method-Override" + private final val overrideParam = "_method" + private final val varyHeader = "Vary" + private final val customHeader = "X-Custom-Header" + + private val headerOverrideStrategy = HeaderOverrideStrategy(CaseInsensitiveString(overrideHeader)) + private val queryOverrideStrategy = QueryOverrideStrategy(overrideParam) + + private val postHeaderOverriderConfig = defaultConfig + private val postQueryOverridersConfig = + HttpMethodOverriderConfig(queryOverrideStrategy, List(POST)) + private val deleteHeaderOverriderConfig = + HttpMethodOverriderConfig(headerOverrideStrategy, List(DELETE)) + private val deleteQueryOverridersConfig = + HttpMethodOverriderConfig(queryOverrideStrategy, List(DELETE)) + private val noMethodHeaderOverriderConfig = + HttpMethodOverriderConfig(headerOverrideStrategy, List.empty) + + private val testApp = Router("/" -> HttpRoutes.of[IO] { + case r @ GET -> Root / "resources" / "id" => + Ok(responseText[IO](msg = "resource's details", r)) + case r @ PUT -> Root / "resources" / "id" => + Ok(responseText(msg = "resource updated", r), Header(varyHeader, customHeader)) + case r @ DELETE -> Root / "resources" / "id" => + Ok(responseText(msg = "resource deleted", r)) + }).orNotFound + + private def mkResponseText( + msg: String, + reqMethod: Method, + overriddenMethod: Option[Method]): String = + overriddenMethod + .map(om => s"[$om ~> $reqMethod] => $msg") + .getOrElse(s"[$reqMethod] => $msg") + + private def responseText[F[_]](msg: String, req: Request[F]): String = { + val overriddenMethod = req.attributes.get(HttpMethodOverrider.overriddenMethodAttrKey) + mkResponseText(msg, req.method, overriddenMethod) + } + + "MethodOverrider middleware" should { + "ignore method override if request method not in the overridable method list" in { + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withMethod(GET) + .withHeaders(Header(overrideHeader, "PUT")) + val app = HttpMethodOverrider(testApp, noMethodHeaderOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource's details", reqMethod = GET, overriddenMethod = None)) + } + + "override request method when using header method overrider strategy if override method provided" in { + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withMethod(POST) + .withHeaders(Header(overrideHeader, "PUT")) + val app = HttpMethodOverrider(testApp, postHeaderOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) + } + + "not override request method when using header method overrider strategy if override method not provided" in { + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withMethod(DELETE) + val app = HttpMethodOverrider(testApp, deleteHeaderOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource deleted", reqMethod = DELETE, overriddenMethod = None)) + } + + "override request method and store the original method if using query method overrider strategy" in { + val req = Request[IO](uri = Uri.uri("/resources/id?_method=PUT")) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postQueryOverridersConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) + } + + "not override request method when using query method overrider strategy if override method not provided" in { + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withMethod(DELETE) + val app = HttpMethodOverrider(testApp, deleteQueryOverridersConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource deleted", reqMethod = DELETE, overriddenMethod = None)) + } + + "return 404 when using header method overrider strategy if override method provided is not recognized" in { + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withMethod(POST) + .withHeaders(Header(overrideHeader, "INVALID")) + val app = HttpMethodOverrider(testApp, postHeaderOverriderConfig) + + val res = app(req) + res must returnStatus(Status.NotFound) + } + + "return 404 when using query method overrider strategy if override method provided is not recognized" in { + val req = Request[IO](uri = Uri.uri("/resources/id?_method=INVALID")) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postQueryOverridersConfig) + + val res = app(req) + res must returnStatus(Status.NotFound) + } + + "return 405 when using header method overrider strategy if override method provided is duped" in { + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withMethod(POST) + .withHeaders(Header(overrideHeader, "")) + val app = HttpMethodOverrider(testApp, postHeaderOverriderConfig) + + val res = app(req) + res must returnStatus(Status.MethodNotAllowed) + } + + "return 405 when using query method overrider strategy if override method provided is duped" in { + val req = Request[IO](uri = Uri.uri("/resources/id?_method=")) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postQueryOverridersConfig) + + val res = app(req) + res must returnStatus(Status.MethodNotAllowed) + } + + "override request method when using header method overrider strategy and be case insensitive" in { + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withMethod(POST) + .withHeaders(Header(overrideHeader, "pUt")) + val app = HttpMethodOverrider(testApp, postHeaderOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) + } + + "override request method when using query method overrider strategy and be case insensitive" in { + val req = Request[IO](uri = Uri.uri("/resources/id?_method=pUt")) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postQueryOverridersConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) + } + + "updates vary header when using query method overrider strategy and vary header comes pre-populated" in { + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withMethod(POST) + .withHeaders(Header(overrideHeader, "PUT")) + val app = HttpMethodOverrider(testApp, postHeaderOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) + + res must returnValue(containHeader(Header(varyHeader, s"$customHeader, $overrideHeader"))) + } + + "set vary header when using query method overrider strategy and vary header has not been set" in { + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withMethod(POST) + .withHeaders(Header(overrideHeader, "DELETE")) + val app = HttpMethodOverrider(testApp, postHeaderOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource deleted", reqMethod = DELETE, overriddenMethod = Some(POST))) + + res must returnValue(containHeader(Header(varyHeader, s"$overrideHeader"))) + } + } +} diff --git a/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala b/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala index 751960a75a7..6ac599aee3a 100644 --- a/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala +++ b/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala @@ -34,6 +34,11 @@ trait Http4sMatchers[F[_]] extends Matchers with RunTimedMatchers[F] { m.headers.aka("the headers") } + def containHeader(h: Header): Matcher[Message[F]] = + beSome(h.value) ^^ { m: Message[F] => + m.headers.get(h.name).map(_.value).aka("the particular header") + } + def haveMediaType(mt: MediaType): Matcher[Message[F]] = beSome(mt) ^^ { m: Message[F] => m.headers.get(`Content-Type`).map(_.mediaType).aka("the media type header") From 71e870372c06dab84c936725e854c3f302961a49 Mon Sep 17 00:00:00 2001 From: Omar Ahmed Date: Sun, 13 Jan 2019 02:21:03 -0500 Subject: [PATCH 02/11] Fix typo --- .../http4s/server/middleware/HttpMethodOverriderSpec.scala | 4 ++-- .../src/main/scala/org/http4s/testing/Http4sMatchers.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala index a1a2c6b246c..4564a4f8823 100644 --- a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala +++ b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala @@ -184,7 +184,7 @@ class HttpMethodOverriderSpec extends Http4sSpec { res must returnBody( mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) - res must returnValue(containHeader(Header(varyHeader, s"$customHeader, $overrideHeader"))) + res must returnValue(containsHeader(Header(varyHeader, s"$customHeader, $overrideHeader"))) } "set vary header when using query method overrider strategy and vary header has not been set" in { @@ -198,7 +198,7 @@ class HttpMethodOverriderSpec extends Http4sSpec { res must returnBody( mkResponseText(msg = "resource deleted", reqMethod = DELETE, overriddenMethod = Some(POST))) - res must returnValue(containHeader(Header(varyHeader, s"$overrideHeader"))) + res must returnValue(containsHeader(Header(varyHeader, s"$overrideHeader"))) } } } diff --git a/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala b/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala index 6ac599aee3a..5c3c03e1be6 100644 --- a/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala +++ b/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala @@ -34,7 +34,7 @@ trait Http4sMatchers[F[_]] extends Matchers with RunTimedMatchers[F] { m.headers.aka("the headers") } - def containHeader(h: Header): Matcher[Message[F]] = + def containsHeader(h: Header): Matcher[Message[F]] = beSome(h.value) ^^ { m: Message[F] => m.headers.get(h.name).map(_.value).aka("the particular header") } From 46c4693e68785b614eabb323a939b09b40fb9fbc Mon Sep 17 00:00:00 2001 From: Omar Ahmed Date: Mon, 14 Jan 2019 23:36:07 -0500 Subject: [PATCH 03/11] Partially apply review suggetions --- .../middleware/HttpMethodOverrider.scala | 70 ++++++++++++++----- .../middleware/HttpMethodOverriderSpec.scala | 4 +- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala index bf3a9d89933..7ea84d2436c 100644 --- a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala +++ b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala @@ -4,25 +4,59 @@ import cats.Applicative import cats.data.Kleisli import cats.implicits._ import org.http4s.util.CaseInsensitiveString -import org.http4s.{AttributeKey, Header, HttpApp, Method, ParseResult, Request, Response, Status} +import org.http4s.{ + AttributeKey, + Header, + Http, + HttpApp, + Method, + ParseResult, + Request, + Response, + Status +} object HttpMethodOverrider { /** * HttpMethodOverrider middleware config options. */ - final case class HttpMethodOverriderConfig( - overrideStrategy: OverrideStrategy, - overridableMethods: List[Method]) + class HttpMethodOverriderConfig( + val overrideStrategy: OverrideStrategy, + val overridableMethods: Set[Method]) { + + type Self = HttpMethodOverriderConfig + + private def copy( + overrideStrategy: OverrideStrategy = overrideStrategy, + overridableMethods: Set[Method] = overridableMethods + ): Self = + new HttpMethodOverriderConfig(overrideStrategy, overridableMethods) + + def withOverrideStrategy(overrideStrategy: OverrideStrategy): Self = + copy(overrideStrategy = overrideStrategy) + + def withOverridableMethods(overridableMethods: Set[Method]): Self = + copy(overridableMethods = overridableMethods) + } + + object HttpMethodOverriderConfig { + def apply( + overrideStrategy: OverrideStrategy, + overridableMethods: Set[Method]): HttpMethodOverriderConfig = + new HttpMethodOverriderConfig(overrideStrategy, overridableMethods) + } sealed trait OverrideStrategy final case class HeaderOverrideStrategy(headerName: CaseInsensitiveString) extends OverrideStrategy final case class QueryOverrideStrategy(paramName: String) extends OverrideStrategy + // TODO: tory to fit this inside the main logic + // final case class FunctionOverrideStrategy[G[_]](fn: Request[G] => Method) extends OverrideStrategy val defaultConfig = HttpMethodOverriderConfig( HeaderOverrideStrategy(CaseInsensitiveString("X-HTTP-Method-Override")), - List(Method.POST)) + Set(Method.POST)) val overriddenMethodAttrKey: AttributeKey[Method] = AttributeKey[Method] @@ -36,17 +70,17 @@ object HttpMethodOverrider { * @param http [[HttpApp]] to transform * @param config http method overrider config */ - def apply[F[_]](http: HttpApp[F], config: HttpMethodOverriderConfig)( - implicit F: Applicative[F]): HttpApp[F] = { + def apply[F[_], G[_]](http: Http[F, G], config: HttpMethodOverriderConfig)( + implicit F: Applicative[F]): Http[F, G] = { def processRequestWithMethod( - req: Request[F], - parseResult: ParseResult[Method]): F[Response[F]] = parseResult match { - case Left(_) => F.pure(Response[F](Status.MethodNotAllowed)) + req: Request[G], + parseResult: ParseResult[Method]): F[Response[G]] = parseResult match { + case Left(_) => F.pure(Response[G](Status.BadRequest)) case Right(om) => http(updateRequestWithMethod(req, om)).map(updateVaryHeader) } - def updateVaryHeader(resp: Response[F]): Response[F] = { + def updateVaryHeader(resp: Response[G]): Response[G] = { val varyHeaderName = CaseInsensitiveString("Vary") config.overrideStrategy match { case HeaderOverrideStrategy(headerName) => @@ -61,24 +95,26 @@ object HttpMethodOverrider { } } - def updateRequestWithMethod(req: Request[F], om: Method): Request[F] = { + def updateRequestWithMethod(req: Request[G], om: Method): Request[G] = { val attrs = req.attributes ++ Seq(overriddenMethodAttrKey(req.method)) req.withAttributes(attrs).withMethod(om) } - def ignoresOverrideIfNotAllowed(req: Request[F]): Option[Unit] = - if (config.overridableMethods.contains(req.method)) Some(()) else None + def ignoresOverrideIfNotAllowed(req: Request[G]): Option[Unit] = + config.overridableMethods.contains(req.method).guard[Option].as(()) + + def parseMethod(m: String): ParseResult[Method] = Method.fromString(m.toUpperCase) - def getUnsafeOverrideMethod(req: Request[F]): Option[String] = + def getUnsafeOverrideMethod(req: Request[G]): Option[String] = config.overrideStrategy match { case HeaderOverrideStrategy(headerName) => req.headers.get(headerName).map(_.value) case QueryOverrideStrategy(parameter) => req.params.get(parameter) } - Kleisli { req: Request[F] => + Kleisli { req: Request[G] => { (ignoresOverrideIfNotAllowed(req) *> getUnsafeOverrideMethod(req)) - .map(m => Method.fromString(m.toUpperCase)) + .map(parseMethod) .map(processRequestWithMethod(req, _)) .getOrElse(http(req)) } diff --git a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala index 4564a4f8823..e69d021ff96 100644 --- a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala +++ b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala @@ -131,14 +131,14 @@ class HttpMethodOverriderSpec extends Http4sSpec { res must returnStatus(Status.NotFound) } - "return 405 when using header method overrider strategy if override method provided is duped" in { + "return 400 when using header method overrider strategy if override method provided is duped" in { val req = Request[IO](uri = Uri.uri("/resources/id")) .withMethod(POST) .withHeaders(Header(overrideHeader, "")) val app = HttpMethodOverrider(testApp, postHeaderOverriderConfig) val res = app(req) - res must returnStatus(Status.MethodNotAllowed) + res must returnStatus(Status.BadRequest) } "return 405 when using query method overrider strategy if override method provided is duped" in { From c088efc6b62815080ab4ba84702720efd5f06584 Mon Sep 17 00:00:00 2001 From: Omar Ahmed Date: Tue, 15 Jan 2019 00:13:48 -0500 Subject: [PATCH 04/11] Fix spec issues --- .../server/middleware/HttpMethodOverrider.scala | 12 +----------- .../server/middleware/HttpMethodOverriderSpec.scala | 12 ++++++------ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala index 7ea84d2436c..edc6ddcb0a7 100644 --- a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala +++ b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala @@ -4,17 +4,7 @@ import cats.Applicative import cats.data.Kleisli import cats.implicits._ import org.http4s.util.CaseInsensitiveString -import org.http4s.{ - AttributeKey, - Header, - Http, - HttpApp, - Method, - ParseResult, - Request, - Response, - Status -} +import org.http4s.{AttributeKey, Header, Http, Method, ParseResult, Request, Response, Status} object HttpMethodOverrider { diff --git a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala index e69d021ff96..497e46e1858 100644 --- a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala +++ b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala @@ -24,13 +24,13 @@ class HttpMethodOverriderSpec extends Http4sSpec { private val postHeaderOverriderConfig = defaultConfig private val postQueryOverridersConfig = - HttpMethodOverriderConfig(queryOverrideStrategy, List(POST)) + HttpMethodOverriderConfig(queryOverrideStrategy, Set(POST)) private val deleteHeaderOverriderConfig = - HttpMethodOverriderConfig(headerOverrideStrategy, List(DELETE)) + HttpMethodOverriderConfig(headerOverrideStrategy, Set(DELETE)) private val deleteQueryOverridersConfig = - HttpMethodOverriderConfig(queryOverrideStrategy, List(DELETE)) + HttpMethodOverriderConfig(queryOverrideStrategy, Set(DELETE)) private val noMethodHeaderOverriderConfig = - HttpMethodOverriderConfig(headerOverrideStrategy, List.empty) + HttpMethodOverriderConfig(headerOverrideStrategy, Set.empty) private val testApp = Router("/" -> HttpRoutes.of[IO] { case r @ GET -> Root / "resources" / "id" => @@ -141,13 +141,13 @@ class HttpMethodOverriderSpec extends Http4sSpec { res must returnStatus(Status.BadRequest) } - "return 405 when using query method overrider strategy if override method provided is duped" in { + "return 400 when using query method overrider strategy if override method provided is duped" in { val req = Request[IO](uri = Uri.uri("/resources/id?_method=")) .withMethod(POST) val app = HttpMethodOverrider(testApp, postQueryOverridersConfig) val res = app(req) - res must returnStatus(Status.MethodNotAllowed) + res must returnStatus(Status.BadRequest) } "override request method when using header method overrider strategy and be case insensitive" in { From 0c2d965cc9e3f9d97aea7b14a3ce00dd34cfeef9 Mon Sep 17 00:00:00 2001 From: Omar Ahmed Date: Sat, 19 Jan 2019 16:42:03 -0500 Subject: [PATCH 05/11] Add Form override strategy and increase test coverage --- .../middleware/HttpMethodOverrider.scala | 73 ++++++--- .../middleware/HttpMethodOverriderSpec.scala | 150 ++++++++++++++++-- .../org/http4s/testing/Http4sMatchers.scala | 6 + 3 files changed, 194 insertions(+), 35 deletions(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala index edc6ddcb0a7..91194f3980c 100644 --- a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala +++ b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala @@ -1,10 +1,23 @@ package org.http4s.server.middleware -import cats.Applicative import cats.data.Kleisli +import cats.effect.Sync import cats.implicits._ +import cats.{Monad, ~>} import org.http4s.util.CaseInsensitiveString -import org.http4s.{AttributeKey, Header, Http, Method, ParseResult, Request, Response, Status} +import org.http4s.{ + AttributeKey, + Header, + Http, + Method, + ParseResult, + Request, + Response, + Status, + UrlForm +} + +import scala.reflect.runtime.universe._ object HttpMethodOverrider { @@ -41,8 +54,10 @@ object HttpMethodOverrider { final case class HeaderOverrideStrategy(headerName: CaseInsensitiveString) extends OverrideStrategy final case class QueryOverrideStrategy(paramName: String) extends OverrideStrategy - // TODO: tory to fit this inside the main logic - // final case class FunctionOverrideStrategy[G[_]](fn: Request[G] => Method) extends OverrideStrategy + final case class FormOverrideStrategy[G[_], F[_]]( + fieldName: String, + naturalTransformation: G ~> F) + extends OverrideStrategy val defaultConfig = HttpMethodOverriderConfig( HeaderOverrideStrategy(CaseInsensitiveString("X-HTTP-Method-Override")), @@ -57,11 +72,19 @@ object HttpMethodOverrider { * the desired one using a custom header or request parameter. The middleware will '''override''' * the original verb with the new one for you, allowing the request the be dispatched properly. * - * @param http [[HttpApp]] to transform + * @param http [[Http]] to transform * @param config http method overrider config */ def apply[F[_], G[_]](http: Http[F, G], config: HttpMethodOverriderConfig)( - implicit F: Applicative[F]): Http[F, G] = { + implicit F: Monad[F], + S: Sync[G], + TT: TypeTag[G ~> F]): Http[F, G] = { + + lazy val runtimeTypeNT = implicitly[TypeTag[G ~> F]].tpe + + val parseMethod = (m: String) => Method.fromString(m.toUpperCase) + + val processRequestWithOriginalMethod = (req: Request[G]) => http(req) def processRequestWithMethod( req: Request[G], @@ -81,7 +104,7 @@ object HttpMethodOverrider { .getOrElse(Header(varyHeaderName.value, headerName.value)) resp.withHeaders(resp.headers.put(updatedVaryHeader)) - case QueryOverrideStrategy(_) => resp + case _ => resp } } @@ -90,23 +113,35 @@ object HttpMethodOverrider { req.withAttributes(attrs).withMethod(om) } - def ignoresOverrideIfNotAllowed(req: Request[G]): Option[Unit] = - config.overridableMethods.contains(req.method).guard[Option].as(()) - - def parseMethod(m: String): ParseResult[Method] = Method.fromString(m.toUpperCase) - - def getUnsafeOverrideMethod(req: Request[G]): Option[String] = + def getUnsafeOverrideMethod(req: Request[G]): F[Option[String]] = config.overrideStrategy match { - case HeaderOverrideStrategy(headerName) => req.headers.get(headerName).map(_.value) - case QueryOverrideStrategy(parameter) => req.params.get(parameter) + case HeaderOverrideStrategy(headerName) => F.pure(req.headers.get(headerName).map(_.value)) + case QueryOverrideStrategy(parameter) => F.pure(req.params.get(parameter)) + case FormOverrideStrategy(field, f) if runtimeTypeNT == typeOf[G ~> F] => + val nt = f.asInstanceOf[G ~> F] + for { + formFields <- nt( + UrlForm + .entityDecoder[G] + .decode(req, strict = true) + .value + .map(_.toOption.map(_.values))) + + } yield formFields.flatMap(_.get(field).flatMap(_.uncons.map(_._1))) } + def processRequest(req: Request[G]): F[Response[G]] = getUnsafeOverrideMethod(req).flatMap { + case Some(m: String) => parseMethod.andThen(processRequestWithMethod(req, _)).apply(m) + case None => processRequestWithOriginalMethod(req) + } + Kleisli { req: Request[G] => { - (ignoresOverrideIfNotAllowed(req) *> getUnsafeOverrideMethod(req)) - .map(parseMethod) - .map(processRequestWithMethod(req, _)) - .getOrElse(http(req)) + config.overridableMethods + .contains(req.method) + .guard[Option] + .as(processRequest(req)) + .getOrElse(processRequestWithOriginalMethod(req)) } } } diff --git a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala index 497e46e1858..589d362a733 100644 --- a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala +++ b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala @@ -1,34 +1,35 @@ package org.http4s.server.middleware import cats.effect.IO +import cats.~> import org.http4s._ import org.http4s.dsl.io._ import org.http4s.server.Router -import org.http4s.server.middleware.HttpMethodOverrider.{ - HeaderOverrideStrategy, - HttpMethodOverriderConfig, - QueryOverrideStrategy, - defaultConfig -} +import org.http4s.server.middleware.HttpMethodOverrider._ import org.http4s.util.CaseInsensitiveString class HttpMethodOverriderSpec extends Http4sSpec { private final val overrideHeader = "X-HTTP-Method-Override" - private final val overrideParam = "_method" + private final val overrideParam, overrideField: String = "_method" private final val varyHeader = "Vary" private final val customHeader = "X-Custom-Header" private val headerOverrideStrategy = HeaderOverrideStrategy(CaseInsensitiveString(overrideHeader)) private val queryOverrideStrategy = QueryOverrideStrategy(overrideParam) + private val formOverrideStrategy = FormOverrideStrategy(overrideParam, λ[IO ~> IO](i => i)) private val postHeaderOverriderConfig = defaultConfig - private val postQueryOverridersConfig = + private val postQueryOverriderConfig = HttpMethodOverriderConfig(queryOverrideStrategy, Set(POST)) + private val postFormOverriderConfig = + HttpMethodOverriderConfig(formOverrideStrategy, Set(POST)) private val deleteHeaderOverriderConfig = HttpMethodOverriderConfig(headerOverrideStrategy, Set(DELETE)) - private val deleteQueryOverridersConfig = + private val deleteQueryOverriderConfig = HttpMethodOverriderConfig(queryOverrideStrategy, Set(DELETE)) + private val deleteFormOverriderConfig = + HttpMethodOverriderConfig(formOverrideStrategy, Set(DELETE)) private val noMethodHeaderOverriderConfig = HttpMethodOverriderConfig(headerOverrideStrategy, Set.empty) @@ -90,10 +91,10 @@ class HttpMethodOverriderSpec extends Http4sSpec { mkResponseText(msg = "resource deleted", reqMethod = DELETE, overriddenMethod = None)) } - "override request method and store the original method if using query method overrider strategy" in { + "override request method and store the original method when using query method overrider strategy" in { val req = Request[IO](uri = Uri.uri("/resources/id?_method=PUT")) .withMethod(POST) - val app = HttpMethodOverrider(testApp, postQueryOverridersConfig) + val app = HttpMethodOverrider(testApp, postQueryOverriderConfig) val res = app(req) res must returnStatus(Status.Ok) @@ -104,7 +105,33 @@ class HttpMethodOverriderSpec extends Http4sSpec { "not override request method when using query method overrider strategy if override method not provided" in { val req = Request[IO](uri = Uri.uri("/resources/id")) .withMethod(DELETE) - val app = HttpMethodOverrider(testApp, deleteQueryOverridersConfig) + val app = HttpMethodOverrider(testApp, deleteQueryOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource deleted", reqMethod = DELETE, overriddenMethod = None)) + } + + "override request method and store the original method when using form method overrider strategy" in { + val urlForm = UrlForm("foo" -> "bar", overrideField -> "PUT") + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withEntity(urlForm) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postFormOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) + } + + "not override request method when using form method overrider strategy if override method not provided" in { + val urlForm = UrlForm("foo" -> "bar") + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withEntity(urlForm) + .withMethod(DELETE) + val app = HttpMethodOverrider(testApp, deleteFormOverriderConfig) val res = app(req) res must returnStatus(Status.Ok) @@ -125,7 +152,18 @@ class HttpMethodOverriderSpec extends Http4sSpec { "return 404 when using query method overrider strategy if override method provided is not recognized" in { val req = Request[IO](uri = Uri.uri("/resources/id?_method=INVALID")) .withMethod(POST) - val app = HttpMethodOverrider(testApp, postQueryOverridersConfig) + val app = HttpMethodOverrider(testApp, postQueryOverriderConfig) + + val res = app(req) + res must returnStatus(Status.NotFound) + } + + "return 404 when using form method overrider strategy if override method provided is not recognized" in { + val urlForm = UrlForm("foo" -> "bar", overrideField -> "INVALID") + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withEntity(urlForm) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postFormOverriderConfig) val res = app(req) res must returnStatus(Status.NotFound) @@ -144,7 +182,18 @@ class HttpMethodOverriderSpec extends Http4sSpec { "return 400 when using query method overrider strategy if override method provided is duped" in { val req = Request[IO](uri = Uri.uri("/resources/id?_method=")) .withMethod(POST) - val app = HttpMethodOverrider(testApp, postQueryOverridersConfig) + val app = HttpMethodOverrider(testApp, postQueryOverriderConfig) + + val res = app(req) + res must returnStatus(Status.BadRequest) + } + + "return 400 when using form method overrider strategy if override method provided is duped" in { + val urlForm = UrlForm("foo" -> "bar", overrideField -> "") + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withEntity(urlForm) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postFormOverriderConfig) val res = app(req) res must returnStatus(Status.BadRequest) @@ -165,7 +214,20 @@ class HttpMethodOverriderSpec extends Http4sSpec { "override request method when using query method overrider strategy and be case insensitive" in { val req = Request[IO](uri = Uri.uri("/resources/id?_method=pUt")) .withMethod(POST) - val app = HttpMethodOverrider(testApp, postQueryOverridersConfig) + val app = HttpMethodOverrider(testApp, postQueryOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) + } + + "override request method when form query method overrider strategy and be case insensitive" in { + val urlForm = UrlForm("foo" -> "bar", overrideField -> "pUt") + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withEntity(urlForm) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postFormOverriderConfig) val res = app(req) res must returnStatus(Status.Ok) @@ -187,7 +249,7 @@ class HttpMethodOverriderSpec extends Http4sSpec { res must returnValue(containsHeader(Header(varyHeader, s"$customHeader, $overrideHeader"))) } - "set vary header when using query method overrider strategy and vary header has not been set" in { + "set vary header when using header method overrider strategy and vary header has not been set" in { val req = Request[IO](uri = Uri.uri("/resources/id")) .withMethod(POST) .withHeaders(Header(overrideHeader, "DELETE")) @@ -200,5 +262,61 @@ class HttpMethodOverriderSpec extends Http4sSpec { res must returnValue(containsHeader(Header(varyHeader, s"$overrideHeader"))) } + + "not set vary header when using query method overrider strategy and vary header has not been set" in { + val req = Request[IO](uri = Uri.uri("/resources/id?_method=DELETE")) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postQueryOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource deleted", reqMethod = DELETE, overriddenMethod = Some(POST))) + + res must returnValue(doesntContainHeader(CaseInsensitiveString(varyHeader))) + } + + "not update vary header when using query method overrider strategy and vary header comes pre-populated" in { + val req = Request[IO](uri = Uri.uri("/resources/id?_method=PUT")) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postQueryOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) + + res must returnValue(containsHeader(Header(varyHeader, s"$customHeader"))) + } + + "not set vary header when using form method overrider strategy and vary header has not been set" in { + val urlForm = UrlForm("foo" -> "bar", overrideField -> "DELETE") + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withEntity(urlForm) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postFormOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource deleted", reqMethod = DELETE, overriddenMethod = Some(POST))) + + res must returnValue(doesntContainHeader(CaseInsensitiveString(varyHeader))) + } + + "not update vary header when using form method overrider strategy and vary header comes pre-populated" in { + val urlForm = UrlForm("foo" -> "bar", overrideField -> "PUT") + val req = Request[IO](uri = Uri.uri("/resources/id")) + .withEntity(urlForm) + .withMethod(POST) + val app = HttpMethodOverrider(testApp, postFormOverriderConfig) + + val res = app(req) + res must returnStatus(Status.Ok) + res must returnBody( + mkResponseText(msg = "resource updated", reqMethod = PUT, overriddenMethod = Some(POST))) + + res must returnValue(containsHeader(Header(varyHeader, s"$customHeader"))) + } } } diff --git a/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala b/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala index 5c3c03e1be6..35638f0e688 100644 --- a/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala +++ b/testing/src/main/scala/org/http4s/testing/Http4sMatchers.scala @@ -4,6 +4,7 @@ package testing import cats.syntax.flatMap._ import cats.data.EitherT import org.http4s.headers._ +import org.http4s.util.CaseInsensitiveString import org.specs2.matcher._ /** This might be useful in a testkit spinoff. Let's see what they do for us. */ @@ -39,6 +40,11 @@ trait Http4sMatchers[F[_]] extends Matchers with RunTimedMatchers[F] { m.headers.get(h.name).map(_.value).aka("the particular header") } + def doesntContainHeader(h: CaseInsensitiveString): Matcher[Message[F]] = + beNone ^^ { m: Message[F] => + m.headers.get(h).aka("the particular header") + } + def haveMediaType(mt: MediaType): Matcher[Message[F]] = beSome(mt) ^^ { m: Message[F] => m.headers.get(`Content-Type`).map(_.mediaType).aka("the media type header") From 091107ffb0836a439e3bc5ab0fe1ffb0aeb42482 Mon Sep 17 00:00:00 2001 From: Omar Ahmed Date: Thu, 31 Jan 2019 22:45:14 -0500 Subject: [PATCH 06/11] Apply nested packages suggestion --- .../server/middleware/HttpMethodOverrider.scala | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala index 91194f3980c..b16391252fd 100644 --- a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala +++ b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala @@ -1,21 +1,13 @@ -package org.http4s.server.middleware +package org.http4s +package server +package middleware import cats.data.Kleisli import cats.effect.Sync import cats.implicits._ import cats.{Monad, ~>} +import org.http4s.Http import org.http4s.util.CaseInsensitiveString -import org.http4s.{ - AttributeKey, - Header, - Http, - Method, - ParseResult, - Request, - Response, - Status, - UrlForm -} import scala.reflect.runtime.universe._ From 11c877448ffe2a433832a4ac465f0c8ba98f70ec Mon Sep 17 00:00:00 2001 From: Omar Ahmed Date: Mon, 4 Feb 2019 16:07:59 -0500 Subject: [PATCH 07/11] Fix broken build when using scala v2.11.12 --- .../org/http4s/server/middleware/HttpMethodOverrider.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala index b16391252fd..ff6c106f755 100644 --- a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala +++ b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala @@ -4,7 +4,10 @@ package middleware import cats.data.Kleisli import cats.effect.Sync -import cats.implicits._ +import cats.instances.option._ +import cats.syntax.functor._ +import cats.syntax.flatMap._ +import cats.syntax.alternative._ import cats.{Monad, ~>} import org.http4s.Http import org.http4s.util.CaseInsensitiveString From 5e516989531ef259c97526509233cff34ea74b18 Mon Sep 17 00:00:00 2001 From: Chris Davenport Date: Fri, 8 Feb 2019 17:37:36 -0500 Subject: [PATCH 08/11] Initial Compile With Type Params --- .../middleware/HttpMethodOverrider.scala | 49 ++++++++----------- .../middleware/HttpMethodOverriderSpec.scala | 22 ++++----- 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala index ff6c106f755..d7d2cf18e37 100644 --- a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala +++ b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala @@ -12,26 +12,24 @@ import cats.{Monad, ~>} import org.http4s.Http import org.http4s.util.CaseInsensitiveString -import scala.reflect.runtime.universe._ - object HttpMethodOverrider { /** * HttpMethodOverrider middleware config options. */ - class HttpMethodOverriderConfig( - val overrideStrategy: OverrideStrategy, + class HttpMethodOverriderConfig[F[_], G[_]]( + val overrideStrategy: OverrideStrategy[F, G], val overridableMethods: Set[Method]) { - type Self = HttpMethodOverriderConfig + type Self = HttpMethodOverriderConfig[F, G] private def copy( - overrideStrategy: OverrideStrategy = overrideStrategy, + overrideStrategy: OverrideStrategy[F, G] = overrideStrategy, overridableMethods: Set[Method] = overridableMethods ): Self = - new HttpMethodOverriderConfig(overrideStrategy, overridableMethods) + new HttpMethodOverriderConfig[F, G](overrideStrategy, overridableMethods) - def withOverrideStrategy(overrideStrategy: OverrideStrategy): Self = + def withOverrideStrategy(overrideStrategy: OverrideStrategy[F, G]): Self = copy(overrideStrategy = overrideStrategy) def withOverridableMethods(overridableMethods: Set[Method]): Self = @@ -39,22 +37,22 @@ object HttpMethodOverrider { } object HttpMethodOverriderConfig { - def apply( - overrideStrategy: OverrideStrategy, - overridableMethods: Set[Method]): HttpMethodOverriderConfig = - new HttpMethodOverriderConfig(overrideStrategy, overridableMethods) + def apply[F[_], G[_]]( + overrideStrategy: OverrideStrategy[F, G], + overridableMethods: Set[Method]): HttpMethodOverriderConfig[F, G] = + new HttpMethodOverriderConfig[F, G](overrideStrategy, overridableMethods) } - sealed trait OverrideStrategy - final case class HeaderOverrideStrategy(headerName: CaseInsensitiveString) - extends OverrideStrategy - final case class QueryOverrideStrategy(paramName: String) extends OverrideStrategy - final case class FormOverrideStrategy[G[_], F[_]]( + sealed trait OverrideStrategy[F[_], G[_]] + final case class HeaderOverrideStrategy[F[_], G[_]](headerName: CaseInsensitiveString) + extends OverrideStrategy[F, G] + final case class QueryOverrideStrategy[F[_], G[_]](paramName: String) extends OverrideStrategy[F, G] + final case class FormOverrideStrategy[F[_], G[_]]( fieldName: String, naturalTransformation: G ~> F) - extends OverrideStrategy + extends OverrideStrategy[F, G] - val defaultConfig = HttpMethodOverriderConfig( + def defaultConfig[F[_], G[_]]: HttpMethodOverriderConfig[F, G] = HttpMethodOverriderConfig[F, G]( HeaderOverrideStrategy(CaseInsensitiveString("X-HTTP-Method-Override")), Set(Method.POST)) @@ -70,12 +68,9 @@ object HttpMethodOverrider { * @param http [[Http]] to transform * @param config http method overrider config */ - def apply[F[_], G[_]](http: Http[F, G], config: HttpMethodOverriderConfig)( + def apply[F[_], G[_]](http: Http[F, G], config: HttpMethodOverriderConfig[F, G])( implicit F: Monad[F], - S: Sync[G], - TT: TypeTag[G ~> F]): Http[F, G] = { - - lazy val runtimeTypeNT = implicitly[TypeTag[G ~> F]].tpe + S: Sync[G]): Http[F, G] = { val parseMethod = (m: String) => Method.fromString(m.toUpperCase) @@ -112,16 +107,14 @@ object HttpMethodOverrider { config.overrideStrategy match { case HeaderOverrideStrategy(headerName) => F.pure(req.headers.get(headerName).map(_.value)) case QueryOverrideStrategy(parameter) => F.pure(req.params.get(parameter)) - case FormOverrideStrategy(field, f) if runtimeTypeNT == typeOf[G ~> F] => - val nt = f.asInstanceOf[G ~> F] + case FormOverrideStrategy(field, f) => for { - formFields <- nt( + formFields <- f( UrlForm .entityDecoder[G] .decode(req, strict = true) .value .map(_.toOption.map(_.values))) - } yield formFields.flatMap(_.get(field).flatMap(_.uncons.map(_._1))) } diff --git a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala index 589d362a733..0bf06ff8c44 100644 --- a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala +++ b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala @@ -15,23 +15,23 @@ class HttpMethodOverriderSpec extends Http4sSpec { private final val varyHeader = "Vary" private final val customHeader = "X-Custom-Header" - private val headerOverrideStrategy = HeaderOverrideStrategy(CaseInsensitiveString(overrideHeader)) - private val queryOverrideStrategy = QueryOverrideStrategy(overrideParam) + private def headerOverrideStrategy[F[_], G[_]] = HeaderOverrideStrategy[F, G](CaseInsensitiveString(overrideHeader)) + private def queryOverrideStrategy[F[_], G[_]] = QueryOverrideStrategy[F, G](overrideParam) private val formOverrideStrategy = FormOverrideStrategy(overrideParam, λ[IO ~> IO](i => i)) - private val postHeaderOverriderConfig = defaultConfig - private val postQueryOverriderConfig = - HttpMethodOverriderConfig(queryOverrideStrategy, Set(POST)) + private def postHeaderOverriderConfig[F[_], G[_]] = defaultConfig[F, G] + private def postQueryOverriderConfig[F[_], G[_]] = + HttpMethodOverriderConfig[F, G](queryOverrideStrategy, Set(POST)) private val postFormOverriderConfig = HttpMethodOverriderConfig(formOverrideStrategy, Set(POST)) - private val deleteHeaderOverriderConfig = - HttpMethodOverriderConfig(headerOverrideStrategy, Set(DELETE)) - private val deleteQueryOverriderConfig = - HttpMethodOverriderConfig(queryOverrideStrategy, Set(DELETE)) + private def deleteHeaderOverriderConfig[F[_], G[_]] = + HttpMethodOverriderConfig[F, G](headerOverrideStrategy, Set(DELETE)) + private def deleteQueryOverriderConfig[F[_], G[_]] = + HttpMethodOverriderConfig[F, G](queryOverrideStrategy, Set(DELETE)) private val deleteFormOverriderConfig = HttpMethodOverriderConfig(formOverrideStrategy, Set(DELETE)) - private val noMethodHeaderOverriderConfig = - HttpMethodOverriderConfig(headerOverrideStrategy, Set.empty) + private def noMethodHeaderOverriderConfig[F[_], G[_]] = + HttpMethodOverriderConfig[F, G](headerOverrideStrategy, Set.empty) private val testApp = Router("/" -> HttpRoutes.of[IO] { case r @ GET -> Root / "resources" / "id" => From b8953dc2a77ab46bcaef9f27446a3f7cbb1e73ae Mon Sep 17 00:00:00 2001 From: Chris Davenport Date: Fri, 8 Feb 2019 17:56:37 -0500 Subject: [PATCH 09/11] Reapply Scalafmt --- .../http4s/server/middleware/HttpMethodOverrider.scala | 10 ++++++---- .../server/middleware/HttpMethodOverriderSpec.scala | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala index d7d2cf18e37..46a2dd55626 100644 --- a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala +++ b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala @@ -46,15 +46,17 @@ object HttpMethodOverrider { sealed trait OverrideStrategy[F[_], G[_]] final case class HeaderOverrideStrategy[F[_], G[_]](headerName: CaseInsensitiveString) extends OverrideStrategy[F, G] - final case class QueryOverrideStrategy[F[_], G[_]](paramName: String) extends OverrideStrategy[F, G] + final case class QueryOverrideStrategy[F[_], G[_]](paramName: String) + extends OverrideStrategy[F, G] final case class FormOverrideStrategy[F[_], G[_]]( fieldName: String, naturalTransformation: G ~> F) extends OverrideStrategy[F, G] - def defaultConfig[F[_], G[_]]: HttpMethodOverriderConfig[F, G] = HttpMethodOverriderConfig[F, G]( - HeaderOverrideStrategy(CaseInsensitiveString("X-HTTP-Method-Override")), - Set(Method.POST)) + def defaultConfig[F[_], G[_]]: HttpMethodOverriderConfig[F, G] = + HttpMethodOverriderConfig[F, G]( + HeaderOverrideStrategy(CaseInsensitiveString("X-HTTP-Method-Override")), + Set(Method.POST)) val overriddenMethodAttrKey: AttributeKey[Method] = AttributeKey[Method] diff --git a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala index 0bf06ff8c44..05bb2e665a0 100644 --- a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala +++ b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala @@ -15,8 +15,9 @@ class HttpMethodOverriderSpec extends Http4sSpec { private final val varyHeader = "Vary" private final val customHeader = "X-Custom-Header" - private def headerOverrideStrategy[F[_], G[_]] = HeaderOverrideStrategy[F, G](CaseInsensitiveString(overrideHeader)) - private def queryOverrideStrategy[F[_], G[_]] = QueryOverrideStrategy[F, G](overrideParam) + private def headerOverrideStrategy[F[_], G[_]] = + HeaderOverrideStrategy[F, G](CaseInsensitiveString(overrideHeader)) + private def queryOverrideStrategy[F[_], G[_]] = QueryOverrideStrategy[F, G](overrideParam) private val formOverrideStrategy = FormOverrideStrategy(overrideParam, λ[IO ~> IO](i => i)) private def postHeaderOverriderConfig[F[_], G[_]] = defaultConfig[F, G] @@ -24,7 +25,7 @@ class HttpMethodOverriderSpec extends Http4sSpec { HttpMethodOverriderConfig[F, G](queryOverrideStrategy, Set(POST)) private val postFormOverriderConfig = HttpMethodOverriderConfig(formOverrideStrategy, Set(POST)) - private def deleteHeaderOverriderConfig[F[_], G[_]] = + private def deleteHeaderOverriderConfig[F[_], G[_]] = HttpMethodOverriderConfig[F, G](headerOverrideStrategy, Set(DELETE)) private def deleteQueryOverriderConfig[F[_], G[_]] = HttpMethodOverriderConfig[F, G](queryOverrideStrategy, Set(DELETE)) From 87dfdf892b66e1d4995345294dad52ed289cc3fd Mon Sep 17 00:00:00 2001 From: Chris Davenport Date: Wed, 13 Feb 2019 16:22:03 -0500 Subject: [PATCH 10/11] Update for Vault Changes --- .../org/http4s/server/middleware/HttpMethodOverrider.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala index 46a2dd55626..745eb11195c 100644 --- a/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala +++ b/server/src/main/scala/org/http4s/server/middleware/HttpMethodOverrider.scala @@ -3,12 +3,13 @@ package server package middleware import cats.data.Kleisli -import cats.effect.Sync +import cats.effect._ import cats.instances.option._ import cats.syntax.functor._ import cats.syntax.flatMap._ import cats.syntax.alternative._ import cats.{Monad, ~>} +import io.chrisdavenport.vault.Key import org.http4s.Http import org.http4s.util.CaseInsensitiveString @@ -58,7 +59,7 @@ object HttpMethodOverrider { HeaderOverrideStrategy(CaseInsensitiveString("X-HTTP-Method-Override")), Set(Method.POST)) - val overriddenMethodAttrKey: AttributeKey[Method] = AttributeKey[Method] + val overriddenMethodAttrKey: Key[Method] = Key.newKey[IO, Method].unsafeRunSync /** Simple middleware for HTTP Method Override. * @@ -101,7 +102,7 @@ object HttpMethodOverrider { } def updateRequestWithMethod(req: Request[G], om: Method): Request[G] = { - val attrs = req.attributes ++ Seq(overriddenMethodAttrKey(req.method)) + val attrs = req.attributes.insert(overriddenMethodAttrKey, req.method) req.withAttributes(attrs).withMethod(om) } From b5d17aa4446cdaa782dc5a284fde8825f699fb82 Mon Sep 17 00:00:00 2001 From: Chris Davenport Date: Wed, 13 Feb 2019 16:54:37 -0500 Subject: [PATCH 11/11] Update tests for Vault --- .../org/http4s/server/middleware/HttpMethodOverriderSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala index 05bb2e665a0..abe7f542e2d 100644 --- a/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala +++ b/server/src/test/scala/org/http4s/server/middleware/HttpMethodOverriderSpec.scala @@ -52,7 +52,7 @@ class HttpMethodOverriderSpec extends Http4sSpec { .getOrElse(s"[$reqMethod] => $msg") private def responseText[F[_]](msg: String, req: Request[F]): String = { - val overriddenMethod = req.attributes.get(HttpMethodOverrider.overriddenMethodAttrKey) + val overriddenMethod = req.attributes.lookup(HttpMethodOverrider.overriddenMethodAttrKey) mkResponseText(msg, req.method, overriddenMethod) }