Skip to content

Commit e887cdb

Browse files
authored
Merge pull request #440 from IRasmivan/api-gateway-v1
Added ApiGatewayProxyHandler for ApiGateWay V1
2 parents 16c9326 + 1b09a46 commit e887cdb

File tree

2 files changed

+163
-1
lines changed

2 files changed

+163
-1
lines changed

lambda-http4s/src/main/scala/feral/lambda/http4s/ApiGatewayProxyHandler.scala

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,104 @@ package feral.lambda
1818
package http4s
1919

2020
import cats.effect.kernel.Concurrent
21+
import cats.syntax.all._
22+
import feral.lambda.ApiGatewayProxyInvocation
23+
import feral.lambda.events.ApiGatewayProxyEvent
24+
import feral.lambda.events.ApiGatewayProxyResult
2125
import feral.lambda.events.ApiGatewayProxyStructuredResultV2
26+
import fs2.Stream
27+
import org.http4s.Charset
28+
import org.http4s.Header
29+
import org.http4s.Headers
2230
import org.http4s.HttpApp
2331
import org.http4s.HttpRoutes
32+
import org.http4s.Method
33+
import org.http4s.Request
34+
import org.http4s.Uri
2435

2536
object ApiGatewayProxyHandler {
37+
38+
def apply[F[_]: Concurrent: ApiGatewayProxyInvocation](
39+
app: HttpApp[F]): F[Option[ApiGatewayProxyResult]] =
40+
for {
41+
event <- Invocation.event
42+
request <- decodeEvent(event)
43+
response <- app(request)
44+
isBase64Encoded = !response.charset.contains(Charset.`UTF-8`)
45+
responseBody <- response
46+
.body
47+
.through(
48+
if (isBase64Encoded) fs2.text.base64.encode else fs2.text.utf8.decode
49+
)
50+
.compile
51+
.string
52+
} yield {
53+
Some(
54+
ApiGatewayProxyResult(
55+
response.status.code,
56+
responseBody,
57+
isBase64Encoded
58+
)
59+
)
60+
}
61+
62+
private[http4s] def decodeEvent[F[_]: Concurrent](
63+
event: ApiGatewayProxyEvent): F[Request[F]] = {
64+
val queryString: String = List(
65+
getQueryStringParameters(event.queryStringParameters),
66+
getMultiValueQueryStringParameters(event.multiValueQueryStringParameters)
67+
).filter(_.nonEmpty).mkString("&")
68+
69+
val uriString: String = event.path + (if (queryString.nonEmpty) s"?$queryString" else "")
70+
71+
for {
72+
method <- Method.fromString(event.httpMethod).liftTo[F]
73+
uri <- Uri.fromString(uriString).liftTo[F]
74+
headers = {
75+
val builder = List.newBuilder[Header.Raw]
76+
event.headers.foreach { h => h.foreachEntry(builder += Header.Raw(_, _)) }
77+
event.multiValueHeaders.foreach { hMap =>
78+
hMap.foreach {
79+
case (key, values) =>
80+
if (!event.headers.exists(_.contains(key))) {
81+
values.foreach(value => builder += Header.Raw(key, value))
82+
}
83+
}
84+
}
85+
Headers(builder.result())
86+
}
87+
readBody =
88+
if (event.isBase64Encoded)
89+
fs2.text.base64.decode[F]
90+
else
91+
fs2.text.utf8.encode[F]
92+
} yield Request(
93+
method,
94+
uri,
95+
headers = headers,
96+
body = Stream.fromOption[F](event.body).through(readBody)
97+
)
98+
}
99+
100+
private def getQueryStringParameters(
101+
queryStringParameters: Option[Map[String, String]]): String =
102+
queryStringParameters.fold("") { params =>
103+
params.map { case (key, value) => s"$key=$value" }.mkString("&")
104+
}
105+
106+
private def getMultiValueQueryStringParameters(
107+
multiValueQueryStringParameters: Option[Map[String, List[String]]]): String =
108+
multiValueQueryStringParameters.fold("") { params =>
109+
params
110+
.flatMap {
111+
case (key, values) =>
112+
values.map(value => s"$key=$value")
113+
}
114+
.mkString("&")
115+
}
116+
26117
@deprecated("Use ApiGatewayProxyHandlerV2", "0.3.0")
27-
def apply[F[_]: Concurrent: ApiGatewayProxyInvocationV2](
118+
def apply[F[_]: ApiGatewayProxyInvocationV2: Concurrent](
28119
routes: HttpRoutes[F]): F[Option[ApiGatewayProxyStructuredResultV2]] = httpRoutes(routes)
29120

30121
@deprecated("Use ApiGatewayProxyHandlerV2", "0.3.0")
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2021 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package feral.lambda
18+
package http4s
19+
20+
import cats.effect.IO
21+
import cats.syntax.all._
22+
import feral.lambda.events.ApiGatewayProxyEvent
23+
import feral.lambda.events.ApiGatewayProxyEventSuite.*
24+
import munit.CatsEffectSuite
25+
import org.http4s.Headers
26+
import org.http4s.HttpApp
27+
import org.http4s.Method
28+
import org.http4s.syntax.all._
29+
30+
class ApiGatewayProxyHandlerSuite extends CatsEffectSuite {
31+
32+
val expectedHeaders: Headers = Headers(
33+
"Accept-Language" -> "en-US,en;q=0.8",
34+
"CloudFront-Is-Mobile-Viewer" -> "false",
35+
"CloudFront-Is-Desktop-Viewer" -> "true",
36+
"Via" -> "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
37+
"X-Amz-Cf-Id" -> "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
38+
"Host" -> "1234567890.execute-api.us-east-1.amazonaws.com",
39+
"Accept-Encoding" -> "gzip, deflate, sdch",
40+
"X-Forwarded-Port" -> "443",
41+
"Cache-Control" -> "max-age=0",
42+
"CloudFront-Viewer-Country" -> "US",
43+
"CloudFront-Is-SmartTV-Viewer" -> "false",
44+
"X-Forwarded-Proto" -> "https",
45+
"Upgrade-Insecure-Requests" -> "1",
46+
"User-Agent" -> "Custom User Agent String",
47+
"CloudFront-Forwarded-Proto" -> "https",
48+
"X-Forwarded-For" -> "127.0.0.1, 127.0.0.2",
49+
"CloudFront-Is-Tablet-Viewer" -> "false",
50+
"Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
51+
)
52+
53+
val expectedBody: String = """{"test":"body"}"""
54+
55+
test("decode event") {
56+
for {
57+
event <- event.as[ApiGatewayProxyEvent].liftTo[IO]
58+
request <- ApiGatewayProxyHandler.decodeEvent[IO](event)
59+
_ <- IO(assertEquals(request.method, Method.POST))
60+
_ <- IO(assertEquals(request.uri, uri"/path/to/resource?foo=bar&foo=bar"))
61+
_ <- IO(assertEquals(request.headers, expectedHeaders))
62+
responseBody <- request.bodyText.compile.string
63+
_ <- IO(assertEquals(responseBody, expectedBody))
64+
} yield ()
65+
}
66+
67+
// compile-only test
68+
def handler(implicit inv: ApiGatewayProxyInvocation[IO]) =
69+
ApiGatewayProxyHandler(HttpApp.notFound[IO])
70+
71+
}

0 commit comments

Comments
 (0)