Skip to content

Commit

Permalink
use aws request and response classes
Browse files Browse the repository at this point in the history
  • Loading branch information
mduesterhoeft committed Mar 6, 2019
1 parent be5e8be commit a2ab411
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 179 deletions.
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ bin/
.idea/
out/

# Package Files
*.jar
*.war
*.eargit

# Serverless directories
.serverless

15 changes: 9 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,19 @@ dependencies {
compile(kotlin("stdlib-jdk8"))
compile(kotlin("reflect"))
compile("com.amazonaws:aws-lambda-java-core:1.2.0")
compile("com.amazonaws:aws-lambda-java-log4j2:1.1.0")
compile("com.fasterxml.jackson.core:jackson-databind:2.9.5")
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.5")
compile("com.amazonaws:aws-lambda-java-events:2.2.5")

compile("org.slf4j:slf4j-api:1.7.26")
compile("com.fasterxml.jackson.core:jackson-databind:2.9.8")
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")
compile("com.google.guava:guava:23.0")
compile("com.google.protobuf:protobuf-java:3.5.1")
compile("com.google.protobuf:protobuf-java-util:3.5.1")
compile("com.google.protobuf:protobuf-java:3.6.1")
compile("com.google.protobuf:protobuf-java-util:3.6.1")

testImplementation("org.junit.jupiter:junit-jupiter-engine:5.3.1")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.4.0")
testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.12")
testImplementation("io.mockk:mockk:1.8.13.kotlin13")
testImplementation("org.slf4j:slf4j-simple:1.7.26")
}

tasks.withType<ShadowJar> {
Expand Down
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m"'

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
Expand Down
2 changes: 1 addition & 1 deletion gradlew.bat
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.github.mduesterhoeft.router

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
import java.util.Base64

fun APIGatewayProxyRequestEvent.acceptHeader() = getHeaderCaseInsensitive("accept")
fun APIGatewayProxyRequestEvent.contentType() = getHeaderCaseInsensitive("content-type")

fun APIGatewayProxyRequestEvent.getHeaderCaseInsensitive(httpHeader: String): String? {
return headers.entries
.firstOrNull { it.key.toLowerCase() == httpHeader.toLowerCase() }
?.value
}

fun APIGatewayProxyResponseEvent.bodyAsBytes() = Base64.getDecoder().decode(body)
38 changes: 0 additions & 38 deletions src/main/kotlin/com/github/mduesterhoeft/router/ApiRequest.kt

This file was deleted.

22 changes: 0 additions & 22 deletions src/main/kotlin/com/github/mduesterhoeft/router/ApiResponse.kt

This file was deleted.

70 changes: 37 additions & 33 deletions src/main/kotlin/com/github/mduesterhoeft/router/RequestHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@ package com.github.mduesterhoeft.router

import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.github.mduesterhoeft.router.ProtoBufUtils.toJsonWithoutWrappers
import com.google.common.net.MediaType
import com.google.protobuf.GeneratedMessageV3
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.Base64
import java.util.logging.LogManager
import kotlin.reflect.KClass
import kotlin.reflect.jvm.reflect

abstract class RequestHandler : RequestHandler<ApiRequest, ApiResponse> {
abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

open val objectMapper = jacksonObjectMapper()

override fun handleRequest(input: ApiRequest, context: Context): ApiResponse? {
log.info("handling request with method '${input.httpMethod}' and path '${input.path}' - Accept:${input.acceptHeader} Content-Type:${input.contentType} $input")
@Suppress("UNCHECKED_CAST")
override fun handleRequest(input: APIGatewayProxyRequestEvent, context: Context): APIGatewayProxyResponseEvent? {
log.info("handling request with method '${input.httpMethod}' and path '${input.path}' - Accept:${input.acceptHeader()} Content-Type:${input.contentType()} $input")
val routes = router.routes as List<RouterFunction<Any, Any>>
val matchResults: List<MatchResult> = routes.map { routerFunction: RouterFunction<Any, Any> ->
val matchResult = routerFunction.requestPredicate.match(input)
Expand All @@ -36,7 +41,7 @@ abstract class RequestHandler : RequestHandler<ApiRequest, ApiResponse> {

private fun deserializeRequest(
handler: HandlerFunction<Any, Any>,
input: ApiRequest
input: APIGatewayProxyRequestEvent
): Any {
val requestType = handler.reflect()!!.parameters.first().type.arguments.first().type?.classifier as KClass<*>
return when (requestType) {
Expand All @@ -46,7 +51,7 @@ abstract class RequestHandler : RequestHandler<ApiRequest, ApiResponse> {
}
}

private fun handleNonDirectMatch(matchResults: List<MatchResult>, input: ApiRequest): ApiResponse {
private fun handleNonDirectMatch(matchResults: List<MatchResult>, input: APIGatewayProxyRequestEvent): APIGatewayProxyResponseEvent {
// no direct match
if (matchResults.any { it.matchPath && it.matchMethod && !it.matchContentType }) {
return createErrorResponse(
Expand Down Expand Up @@ -86,44 +91,43 @@ abstract class RequestHandler : RequestHandler<ApiRequest, ApiResponse> {

abstract val router: Router

open fun createErrorResponse(input: ApiRequest, ex: ApiException): ApiResponse =
ApiJsonResponse(
statusCode = ex.statusCode,
headers = mapOf("Content-Type" to "application/json"),
body = objectMapper.writeValueAsString(mapOf(
open fun createErrorResponse(input: APIGatewayProxyRequestEvent, ex: ApiException): APIGatewayProxyResponseEvent =
APIGatewayProxyResponseEvent()
.withBody(objectMapper.writeValueAsString(mapOf(
"message" to ex.message,
"code" to ex.errorCode,
"details" to ex.details
))
)
)))
.withStatusCode(ex.statusCode)
.withHeaders(mapOf("Content-Type" to "application/json"))

open fun <T> createResponse(input: ApiRequest, response: ResponseEntity<T>): ApiResponse {
val accept = MediaType.parse(input.acceptHeader)
open fun <T> createResponse(input: APIGatewayProxyRequestEvent, response: ResponseEntity<T>): APIGatewayProxyResponseEvent {
val accept = MediaType.parse(input.acceptHeader())
return when {
response.body is Unit -> ApiJsonResponse(statusCode = 204, body = null)
accept.`is`(MediaType.parse("application/x-protobuf")) -> ApiProtoResponse(
statusCode = 200,
headers = mapOf("Accept" to "application/x-protobuf"),
body = (response.body as GeneratedMessageV3).toByteArray()
)
response.body is Unit -> APIGatewayProxyResponseEvent()
.withStatusCode(204)

accept.`is`(MediaType.parse("application/x-protobuf")) -> APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(Base64.getEncoder().encodeToString((response.body as GeneratedMessageV3).toByteArray()))
.withHeaders(mapOf("Content-Type" to "application/x-protobuf"))

accept.`is`(MediaType.parse("application/json")) ->
if (response.body is GeneratedMessageV3)
ApiJsonResponse(
statusCode = 200,
headers = mapOf("Content-Type" to "application/json"),
body = toJsonWithoutWrappers(response.body)
)
APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(toJsonWithoutWrappers(response.body))
.withHeaders(mapOf("Content-Type" to "application/json"))
else
ApiJsonResponse(
statusCode = 200,
headers = mapOf("Content-Type" to "application/json"),
body = response.body?.let { objectMapper.writeValueAsString(it) }
)
APIGatewayProxyResponseEvent()
.withStatusCode(200)
.withBody(response.body?.let { objectMapper.writeValueAsString(it) })
.withHeaders(mapOf("Content-Type" to "application/json"))
else -> throw IllegalArgumentException("unsupported response $response")
}
}

companion object {
val log: Logger = LogManager.getLogger(RequestHandler::class.java)
val log: Logger = LoggerFactory.getLogger(RequestHandler::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.mduesterhoeft.router

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import com.google.common.net.MediaType

data class RequestPredicate(
Expand All @@ -16,22 +17,16 @@ data class RequestPredicate(
produces = mediaTypes.toSet()
}

internal fun match(request: ApiRequest) =
internal fun match(request: APIGatewayProxyRequestEvent) =
MatchResult(
matchPath = pathMatches(request),
matchMethod = methodMatches(request),
matchAcceptType = contentTypeMatches(
request.headers.entries.firstOrNull { it.key.toLowerCase() == "accept" }?.value,
produces
),
matchContentType = contentTypeMatches(
request.headers.entries.firstOrNull { it.key.toLowerCase() == "content-type" }?.value,
consumes
)
matchAcceptType = contentTypeMatches(request.acceptHeader(), produces),
matchContentType = contentTypeMatches(request.contentType(), consumes)
)

private fun pathMatches(request: ApiRequest) = request.path == pathPattern
private fun methodMatches(request: ApiRequest) = method.equals(request.httpMethod, true)
private fun pathMatches(request: APIGatewayProxyRequestEvent) = request.path == pathPattern
private fun methodMatches(request: APIGatewayProxyRequestEvent) = method.equals(request.httpMethod, true)
private fun contentTypeMatches(contentType: String?, accepted: Set<String>) =
if (accepted.isEmpty() && contentType == null) true
else if (contentType == null) false
Expand Down
4 changes: 3 additions & 1 deletion src/main/kotlin/com/github/mduesterhoeft/router/Router.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.github.mduesterhoeft.router

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent

class Router {

val routes = mutableListOf<RouterFunction<*, *>>()
Expand Down Expand Up @@ -38,4 +40,4 @@ data class RouterFunction<I, T>(
val handler: HandlerFunction<I, T>
)

data class Request<I>(val apiRequest: ApiRequest, val body: I)
data class Request<I>(val apiRequest: APIGatewayProxyRequestEvent, val body: I)
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@ package com.github.mduesterhoeft.router

import assertk.assert
import assertk.assertions.isEqualTo
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import org.junit.jupiter.api.Test

class ApiRequestTest {

@Test
fun `should match header`() {
val request = ApiRequest(headers = mutableMapOf("Accept" to "application/json"))
val request = APIGatewayProxyRequestEvent().withHeaders(mapOf("Accept" to "application/json"))

assert(request.acceptHeader).isEqualTo("application/json")
assert(request.acceptHeader()).isEqualTo("application/json")
}

@Test
fun `should match header lowercase`() {
val request = ApiRequest(headers = mutableMapOf("accept" to "application/json"))
val request = APIGatewayProxyRequestEvent().withHeaders(mapOf("accept" to "application/json"))

assert(request.acceptHeader).isEqualTo("application/json")
assert(request.acceptHeader()).isEqualTo("application/json")
}

@Test
fun `should match header uppercase`() {
val request = ApiRequest(headers = mutableMapOf("ACCEPT" to "application/json"))
val request = APIGatewayProxyRequestEvent().withHeaders(mapOf("ACCEPT" to "application/json"))

assert(request.acceptHeader).isEqualTo("application/json")
assert(request.acceptHeader()).isEqualTo("application/json")
}
}
Loading

0 comments on commit a2ab411

Please sign in to comment.