Skip to content

Commit

Permalink
Merge pull request #440 from IRasmivan/api-gateway-v1
Browse files Browse the repository at this point in the history
Added ApiGatewayProxyHandler for ApiGateWay V1
  • Loading branch information
armanbilge authored Dec 6, 2023
2 parents 16c9326 + 1b09a46 commit e887cdb
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,104 @@ package feral.lambda
package http4s

import cats.effect.kernel.Concurrent
import cats.syntax.all._
import feral.lambda.ApiGatewayProxyInvocation
import feral.lambda.events.ApiGatewayProxyEvent
import feral.lambda.events.ApiGatewayProxyResult
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

object ApiGatewayProxyHandler {

def apply[F[_]: Concurrent: ApiGatewayProxyInvocation](
app: HttpApp[F]): F[Option[ApiGatewayProxyResult]] =
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 {
Some(
ApiGatewayProxyResult(
response.status.code,
responseBody,
isBase64Encoded
)
)
}

private[http4s] def decodeEvent[F[_]: Concurrent](
event: ApiGatewayProxyEvent): F[Request[F]] = {
val queryString: String = List(
getQueryStringParameters(event.queryStringParameters),
getMultiValueQueryStringParameters(event.multiValueQueryStringParameters)
).filter(_.nonEmpty).mkString("&")

val uriString: String = event.path + (if (queryString.nonEmpty) s"?$queryString" else "")

for {
method <- Method.fromString(event.httpMethod).liftTo[F]
uri <- Uri.fromString(uriString).liftTo[F]
headers = {
val builder = List.newBuilder[Header.Raw]
event.headers.foreach { h => h.foreachEntry(builder += Header.Raw(_, _)) }
event.multiValueHeaders.foreach { hMap =>
hMap.foreach {
case (key, values) =>
if (!event.headers.exists(_.contains(key))) {
values.foreach(value => builder += Header.Raw(key, value))
}
}
}
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)
)
}

private def getQueryStringParameters(
queryStringParameters: Option[Map[String, String]]): String =
queryStringParameters.fold("") { params =>
params.map { case (key, value) => s"$key=$value" }.mkString("&")
}

private def getMultiValueQueryStringParameters(
multiValueQueryStringParameters: Option[Map[String, List[String]]]): String =
multiValueQueryStringParameters.fold("") { params =>
params
.flatMap {
case (key, values) =>
values.map(value => s"$key=$value")
}
.mkString("&")
}

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

@deprecated("Use ApiGatewayProxyHandlerV2", "0.3.0")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.IO
import cats.syntax.all._
import feral.lambda.events.ApiGatewayProxyEvent
import feral.lambda.events.ApiGatewayProxyEventSuite.*
import munit.CatsEffectSuite
import org.http4s.Headers
import org.http4s.HttpApp
import org.http4s.Method
import org.http4s.syntax.all._

class ApiGatewayProxyHandlerSuite extends CatsEffectSuite {

val expectedHeaders: Headers = Headers(
"Accept-Language" -> "en-US,en;q=0.8",
"CloudFront-Is-Mobile-Viewer" -> "false",
"CloudFront-Is-Desktop-Viewer" -> "true",
"Via" -> "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id" -> "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
"Host" -> "1234567890.execute-api.us-east-1.amazonaws.com",
"Accept-Encoding" -> "gzip, deflate, sdch",
"X-Forwarded-Port" -> "443",
"Cache-Control" -> "max-age=0",
"CloudFront-Viewer-Country" -> "US",
"CloudFront-Is-SmartTV-Viewer" -> "false",
"X-Forwarded-Proto" -> "https",
"Upgrade-Insecure-Requests" -> "1",
"User-Agent" -> "Custom User Agent String",
"CloudFront-Forwarded-Proto" -> "https",
"X-Forwarded-For" -> "127.0.0.1, 127.0.0.2",
"CloudFront-Is-Tablet-Viewer" -> "false",
"Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
)

val expectedBody: String = """{"test":"body"}"""

test("decode event") {
for {
event <- event.as[ApiGatewayProxyEvent].liftTo[IO]
request <- ApiGatewayProxyHandler.decodeEvent[IO](event)
_ <- IO(assertEquals(request.method, Method.POST))
_ <- IO(assertEquals(request.uri, uri"/path/to/resource?foo=bar&foo=bar"))
_ <- IO(assertEquals(request.headers, expectedHeaders))
responseBody <- request.bodyText.compile.string
_ <- IO(assertEquals(responseBody, expectedBody))
} yield ()
}

// compile-only test
def handler(implicit inv: ApiGatewayProxyInvocation[IO]) =
ApiGatewayProxyHandler(HttpApp.notFound[IO])

}

0 comments on commit e887cdb

Please sign in to comment.