Skip to content

Commit

Permalink
Merge pull request #439 from armanbilge/topic/api-gateway-proxy-handl…
Browse files Browse the repository at this point in the history
…er-refactor

`ApiGatewayProxyHandler` -> `ApiGatewayProxyHandlerV2`
  • Loading branch information
armanbilge authored Dec 2, 2023
2 parents 6ee4d75 + 5ccf4a9 commit 16c9326
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 88 deletions.
9 changes: 5 additions & 4 deletions examples/src/main/scala/feral/examples/Http4sLambda.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import feral.lambda.http4s._
import natchez.Trace
import natchez.http4s.NatchezMiddleware
import natchez.xray.XRay
import org.http4s.HttpApp
import org.http4s.HttpRoutes
import org.http4s.client.Client
import org.http4s.dsl.Http4sDsl
Expand Down Expand Up @@ -63,15 +64,15 @@ object http4sHandler
TracedHandler(entrypoint) { implicit trace =>
val tracedClient = NatchezMiddleware.client(client)

// a "middleware" that converts an HttpRoutes into a ApiGatewayProxyHandler
ApiGatewayProxyHandler(myRoutes[IO](tracedClient))
// a "middleware" that converts an HttpApp into a ApiGatewayProxyHandler
ApiGatewayProxyHandlerV2(myApp[IO](tracedClient))
}
}

/**
* Nothing special about this method, including its existence, just an example :)
*/
def myRoutes[F[_]: Concurrent: Trace](client: Client[F]): HttpRoutes[F] = {
def myApp[F[_]: Concurrent: Trace](client: Client[F]): HttpApp[F] = {
implicit val dsl = Http4sDsl[F]
import dsl._

Expand All @@ -80,7 +81,7 @@ object http4sHandler
case GET -> Root / "joke" => Ok(client.expect[String](uri"icanhazdadjoke.com"))
}

NatchezMiddleware.server(routes)
NatchezMiddleware.server(routes).orNotFound
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,82 +18,22 @@ package feral.lambda
package http4s

import cats.effect.kernel.Concurrent
import cats.syntax.all._
import feral.lambda.events.ApiGatewayProxyEventV2
import feral.lambda.events.ApiGatewayProxyStructuredResultV2
import fs2.Stream
import org.http4s.Charset
import org.http4s.Header
import org.http4s.Headers
import org.http4s.HttpApp
import org.http4s.HttpRoutes
import org.http4s.Method
import org.http4s.Request
import org.http4s.Uri
import org.http4s.headers.Cookie
import org.http4s.headers.`Set-Cookie`

object ApiGatewayProxyHandler {
def apply[F[_]: Concurrent: ApiGatewayProxyV2Invocation](
@deprecated("Use ApiGatewayProxyHandlerV2", "0.3.0")
def apply[F[_]: Concurrent: ApiGatewayProxyInvocationV2](
routes: HttpRoutes[F]): F[Option[ApiGatewayProxyStructuredResultV2]] = httpRoutes(routes)

def httpRoutes[F[_]: Concurrent: ApiGatewayProxyV2Invocation](
routes: HttpRoutes[F]): F[Option[ApiGatewayProxyStructuredResultV2]] = httpApp(
routes.orNotFound)
@deprecated("Use ApiGatewayProxyHandlerV2", "0.3.0")
def httpRoutes[F[_]: Concurrent: ApiGatewayProxyInvocationV2](
routes: HttpRoutes[F]): F[Option[ApiGatewayProxyStructuredResultV2]] =
ApiGatewayProxyHandlerV2.httpRoutes(routes)

def httpApp[F[_]: Concurrent: ApiGatewayProxyV2Invocation](
@deprecated("Use ApiGatewayProxyHandlerV2", "0.3.0")
def httpApp[F[_]: Concurrent: ApiGatewayProxyInvocationV2](
app: HttpApp[F]): F[Option[ApiGatewayProxyStructuredResultV2]] =
for {
event <- Invocation.event
request <- decodeEvent(event)
response <- app(request)
isBase64Encoded = !response.charset.contains(Charset.`UTF-8`)
responseBody <- response
.body
.through(
if (isBase64Encoded) fs2.text.base64.encode else fs2.text.utf8.decode
)
.compile
.string
} yield {
val headers = response.headers.headers.groupMap(_.name)(_.value)
Some(
ApiGatewayProxyStructuredResultV2(
response.status.code,
(headers - `Set-Cookie`.name).map {
case (name, values) =>
name -> values.mkString(",")
},
responseBody,
isBase64Encoded,
headers.getOrElse(`Set-Cookie`.name, Nil)
)
)
}

private[http4s] def decodeEvent[F[_]: Concurrent](
event: ApiGatewayProxyEventV2): F[Request[F]] = for {
method <- Method.fromString(event.requestContext.http.method).liftTo[F]
uri <- Uri.fromString(event.rawPath + "?" + event.rawQueryString).liftTo[F]
headers = {
val builder = List.newBuilder[Header.Raw]

event.headers.foreachEntry(builder += Header.Raw(_, _))
event.cookies.filter(_.nonEmpty).foreach { cs =>
builder += Header.Raw(Cookie.name, cs.mkString("; "))
}

Headers(builder.result())
}
readBody =
if (event.isBase64Encoded)
fs2.text.base64.decode[F]
else
fs2.text.utf8.encode[F]
} yield Request(
method,
uri,
headers = headers,
body = Stream.fromOption[F](event.body).through(readBody)
)
ApiGatewayProxyHandlerV2(app)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2021 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package feral.lambda
package http4s

import cats.effect.kernel.Concurrent
import cats.syntax.all._
import feral.lambda.events.ApiGatewayProxyEventV2
import feral.lambda.events.ApiGatewayProxyStructuredResultV2
import fs2.Stream
import org.http4s.Charset
import org.http4s.Header
import org.http4s.Headers
import org.http4s.HttpApp
import org.http4s.HttpRoutes
import org.http4s.Method
import org.http4s.Request
import org.http4s.Uri
import org.http4s.headers.Cookie
import org.http4s.headers.`Set-Cookie`

object ApiGatewayProxyHandlerV2 {

@deprecated("Use apply(routes.orNotFound)", "0.3.0")
def httpRoutes[F[_]: Concurrent: ApiGatewayProxyInvocationV2](
routes: HttpRoutes[F]): F[Option[ApiGatewayProxyStructuredResultV2]] = apply(
routes.orNotFound)

def apply[F[_]: Concurrent: ApiGatewayProxyInvocationV2](
app: HttpApp[F]): F[Option[ApiGatewayProxyStructuredResultV2]] =
for {
event <- Invocation.event
request <- decodeEvent(event)
response <- app(request)
isBase64Encoded = !response.charset.contains(Charset.`UTF-8`)
responseBody <- response
.body
.through(
if (isBase64Encoded) fs2.text.base64.encode else fs2.text.utf8.decode
)
.compile
.string
} yield {
val headers = response.headers.headers.groupMap(_.name)(_.value)
Some(
ApiGatewayProxyStructuredResultV2(
response.status.code,
(headers - `Set-Cookie`.name).map {
case (name, values) =>
name -> values.mkString(",")
},
responseBody,
isBase64Encoded,
headers.getOrElse(`Set-Cookie`.name, Nil)
)
)
}

private[http4s] def decodeEvent[F[_]: Concurrent](
event: ApiGatewayProxyEventV2): F[Request[F]] = for {
method <- Method.fromString(event.requestContext.http.method).liftTo[F]
uri <- Uri.fromString(event.rawPath + "?" + event.rawQueryString).liftTo[F]
headers = {
val builder = List.newBuilder[Header.Raw]

event.headers.foreachEntry(builder += Header.Raw(_, _))
event.cookies.filter(_.nonEmpty).foreach { cs =>
builder += Header.Raw(Cookie.name, cs.mkString("; "))
}

Headers(builder.result())
}
readBody =
if (event.isBase64Encoded)
fs2.text.base64.decode[F]
else
fs2.text.utf8.encode[F]
} yield Request(
method,
uri,
headers = headers,
body = Stream.fromOption[F](event.body).through(readBody)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ import org.http4s.Headers
import org.http4s.Method
import org.http4s.syntax.all._

class ApiGatewayProxyHandlerSuite extends CatsEffectSuite {
class ApiGatewayProxyHandlerV2Suite extends CatsEffectSuite {

import ApiGatewayProxyEventV2Suite._

test("decode event") {
for {
event <- event.as[ApiGatewayProxyEventV2].liftTo[IO]
request <- ApiGatewayProxyHandler.decodeEvent[IO](event)
request <- ApiGatewayProxyHandlerV2.decodeEvent[IO](event)
_ <- IO(assertEquals(request.method, Method.GET))
_ <- IO(assertEquals(request.uri, uri"/default/nodejs-apig-function-1G3XMPLZXVXYI?"))
_ <- IO(assert(request.cookies.nonEmpty))
Expand All @@ -44,7 +44,7 @@ class ApiGatewayProxyHandlerSuite extends CatsEffectSuite {
test("decode event with no cookies") {
for {
event <- eventNoCookies.as[ApiGatewayProxyEventV2].liftTo[IO]
request <- ApiGatewayProxyHandler.decodeEvent[IO](event)
request <- ApiGatewayProxyHandlerV2.decodeEvent[IO](event)
_ <- IO(assert(request.cookies.isEmpty))
_ <- request.body.compile.count.assertEquals(0L)
} yield ()
Expand Down
6 changes: 3 additions & 3 deletions lambda/shared/src/main/scala-2/feral/lambda/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ package object lambda {
implicit val nothingEncoder: Encoder[INothing] = identity(_)

type ApiGatewayProxyInvocation[F[_]] = Invocation[F, ApiGatewayProxyEvent]
type ApiGatewayProxyV2Invocation[F[_]] = Invocation[F, ApiGatewayProxyEventV2]
type ApiGatewayProxyInvocationV2[F[_]] = Invocation[F, ApiGatewayProxyEventV2]
type DynamoDbStreamInvocation[F[_]] = Invocation[F, DynamoDbStreamEvent]
type S3Invocation[F[_]] = Invocation[F, S3Event]
type S3BatchInvocation[F[_]] = Invocation[F, S3BatchEvent]
Expand All @@ -51,8 +51,8 @@ package object lambda {
@deprecated("Renamed to Invocation", "0.3.0")
val LambdaEnv = Invocation

@deprecated("Renamed to ApiGatewayProxyV2Invocation", "0.3.0")
type ApiGatewayProxyLambdaEnv[F[_]] = ApiGatewayProxyV2Invocation[F]
@deprecated("Renamed to ApiGatewayProxyInvocationV2", "0.3.0")
type ApiGatewayProxyLambdaEnv[F[_]] = ApiGatewayProxyInvocationV2[F]
@deprecated("Renamed to DynamoDbStreamInvocation", "0.3.0")
type DynamoDbStreamLambdaEnv[F[_]] = DynamoDbStreamInvocation[F]
@deprecated(
Expand Down
6 changes: 3 additions & 3 deletions lambda/shared/src/main/scala-3/feral/lambda/invocations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package feral.lambda
import events._

type ApiGatewayProxyInvocation[F[_]] = Invocation[F, ApiGatewayProxyEvent]
type ApiGatewayProxyV2Invocation[F[_]] = Invocation[F, ApiGatewayProxyEventV2]
type ApiGatewayProxyInvocationV2[F[_]] = Invocation[F, ApiGatewayProxyEventV2]
type DynamoDbStreamInvocation[F[_]] = Invocation[F, DynamoDbStreamEvent]
type S3Invocation[F[_]] = Invocation[F, S3Event]
type S3BatchInvocation[F[_]] = Invocation[F, S3BatchEvent]
Expand All @@ -31,8 +31,8 @@ type LambdaEnv[F[_], Event] = Invocation[F, Event]
@deprecated("Renamed to Invocation", "0.3.0")
val LambdaEnv = Invocation

@deprecated("Renamed to ApiGatewayProxyV2Invocation", "0.3.0")
type ApiGatewayProxyLambdaEnv[F[_]] = ApiGatewayProxyV2Invocation[F]
@deprecated("Renamed to ApiGatewayProxyInvocationV2", "0.3.0")
type ApiGatewayProxyLambdaEnv[F[_]] = ApiGatewayProxyInvocationV2[F]
@deprecated("Renamed to DynamoDbStreamInvocation", "0.3.0")
type DynamoDbStreamLambdaEnv[F[_]] = DynamoDbStreamInvocation[F]
@deprecated(
Expand Down
9 changes: 8 additions & 1 deletion scalafix/input/src/main/scala/example/V0_3_0Rewrites.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package example

// format: off
import cats.effect.Concurrent
import feral.lambda.LambdaEnv
import feral.lambda.ApiGatewayProxyLambdaEnv
import feral.lambda.DynamoDbStreamLambdaEnv
Expand All @@ -11,6 +12,9 @@ import feral.lambda.SnsLambdaEnv
import feral.lambda.SqsLambdaEnv
import feral.lambda.events.APIGatewayProxyRequestEvent
import feral.lambda.events.APIGatewayProxyResponseEvent
import feral.lambda.events.ApiGatewayProxyStructuredResultV2
import feral.lambda.http4s.ApiGatewayProxyHandler
import org.http4s.HttpApp
// format: on

class Foo[F[_], E] {
Expand All @@ -20,7 +24,10 @@ class Foo[F[_], E] {
}

object Handlers {
def handler1[F[_]](implicit env: ApiGatewayProxyLambdaEnv[F]): Unit = ???
def handler1[F[_]: Concurrent](
implicit env: ApiGatewayProxyLambdaEnv[F]
): F[Option[ApiGatewayProxyStructuredResultV2]] =
ApiGatewayProxyHandler.httpApp(HttpApp.notFound)
def handler2[F[_]](implicit env: DynamoDbStreamLambdaEnv[F]): Unit = ???
def handler3[F[_]](implicit env: S3BatchLambdaEnv[F]): Unit = ???
def handler4[F[_]](implicit env: SnsLambdaEnv[F]): Unit = ???
Expand Down
11 changes: 9 additions & 2 deletions scalafix/output/src/main/scala/example/V0_3_0Rewrites.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package example

// format: off
import feral.lambda.{ ApiGatewayProxyV2Invocation, DynamoDbStreamInvocation, Invocation, S3BatchInvocation, SnsInvocation, SqsInvocation }
import cats.effect.Concurrent
import feral.lambda.events.ApiGatewayProxyStructuredResultV2
import org.http4s.HttpApp
import feral.lambda.{ ApiGatewayProxyInvocationV2, DynamoDbStreamInvocation, Invocation, S3BatchInvocation, SnsInvocation, SqsInvocation }
import feral.lambda.events.{ ApiGatewayProxyEvent, ApiGatewayProxyResult }
import feral.lambda.http4s.ApiGatewayProxyHandlerV2
// format: on

class Foo[F[_], E] {
Expand All @@ -12,7 +16,10 @@ class Foo[F[_], E] {
}

object Handlers {
def handler1[F[_]](implicit env: ApiGatewayProxyV2Invocation[F]): Unit = ???
def handler1[F[_]: Concurrent](
implicit env: ApiGatewayProxyInvocationV2[F]
): F[Option[ApiGatewayProxyStructuredResultV2]] =
ApiGatewayProxyHandlerV2.apply(HttpApp.notFound)
def handler2[F[_]](implicit env: DynamoDbStreamInvocation[F]): Unit = ???
def handler3[F[_]](implicit env: S3BatchInvocation[F]): Unit = ???
def handler4[F[_]](implicit env: SnsInvocation[F]): Unit = ???
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ class V0_3_0Rewrites extends SemanticRule("V0_3_0Rewrites") {
override def fix(implicit doc: SemanticDocument): Patch =
Patch.replaceSymbols(
"feral.lambda.LambdaEnv" -> "feral.lambda.Invocation",
"feral.lambda.ApiGatewayProxyLambdaEnv" -> "feral.lambda.ApiGatewayProxyV2Invocation",
"feral.lambda.ApiGatewayProxyLambdaEnv" -> "feral.lambda.ApiGatewayProxyInvocationV2",
"feral.lambda.DynamoDbStreamLambdaEnv" -> "feral.lambda.DynamoDbStreamInvocation",
"feral.lambda.S3BatchLambdaEnv" -> "feral.lambda.S3BatchInvocation",
"feral.lambda.SnsLambdaEnv" -> "feral.lambda.SnsInvocation",
"feral.lambda.SqsLambdaEnv" -> "feral.lambda.SqsInvocation",
"feral.lambda.events.APIGatewayProxyRequestEvent" -> "feral.lambda.events.ApiGatewayProxyEvent",
"feral.lambda.events.APIGatewayProxyResponseEvent" -> "feral.lambda.events.ApiGatewayProxyResult"
)
"feral.lambda.events.APIGatewayProxyResponseEvent" -> "feral.lambda.events.ApiGatewayProxyResult",
"feral.lambda.http4s.ApiGatewayProxyHandler.httpApp" -> "feral.lambda.http4s.ApiGatewayProxyHandlerV2.apply",
"feral.lambda.http4s.ApiGatewayProxyHandler.apply" -> "feral.lambda.http4s.ApiGatewayProxyHandlerV2.httpRoutes",
"feral.lambda.http4s.ApiGatewayProxyHandler.httpRoutes" -> "feral.lambda.http4s.ApiGatewayProxyHandlerV2.httpRoutes"
) + Patch.removeGlobalImport(Symbol("feral/lambda/http4s/ApiGatewayProxyHandler."))
}

0 comments on commit 16c9326

Please sign in to comment.