Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package io.moia.router.proto

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import io.moia.router.DeserializationHandler
import io.moia.router.contentType
import com.google.common.net.MediaType
import com.google.protobuf.Parser
import io.moia.router.DeserializationHandler
import io.moia.router.contentType
import isCompatibleWith
import java.util.Base64
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.staticFunctions

class ProtoDeserializationHandler : DeserializationHandler {
private val proto = MediaType.parse("application/x-protobuf")
private val protoStructuredSuffixWildcard = MediaType.parse("application/*+x-protobuf")

override fun supports(input: APIGatewayProxyRequestEvent): Boolean =
input.contentType() != null && MediaType.parse(input.contentType()).`is`(proto)
if (input.contentType() == null)
false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you are missing this test case. I was going to write it for you but I'm having that weird kotlin overload problem

else {
MediaType.parse(input.contentType()).let { proto.isCompatibleWith(it) || protoStructuredSuffixWildcard.isCompatibleWith(it) }
}

override fun deserialize(input: APIGatewayProxyRequestEvent, target: KType?): Any {
val bytes = Base64.getDecoder().decode(input.body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ package io.moia.router.proto
import com.google.common.net.MediaType
import com.google.protobuf.GeneratedMessageV3
import io.moia.router.SerializationHandler
import isCompatibleWith
import java.util.Base64

class ProtoSerializationHandler : SerializationHandler {

private val json = MediaType.parse("application/json")
private val jsonStructuredSuffixWildcard = MediaType.parse("application/*+json")

override fun supports(acceptHeader: MediaType, body: Any): Boolean =
body is GeneratedMessageV3

override fun serialize(acceptHeader: MediaType, body: Any): String {
val message = body as GeneratedMessageV3
return if (acceptHeader.`is`(json)) {
return if (json.isCompatibleWith(acceptHeader) || jsonStructuredSuffixWildcard.isCompatibleWith(acceptHeader)) {
ProtoBufUtils.toJsonWithoutWrappers(message)
} else {
Base64.getEncoder().encodeToString(message.toByteArray())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ internal class ProtoDeserializationHandlerTest {
@Test
fun `Deserializer should support if the content type of the input is protobuf`() {
assertTrue(ProtoDeserializationHandler().supports(APIGatewayProxyRequestEvent().withHeader("content-type", "application/x-protobuf")))
assertTrue(ProtoDeserializationHandler().supports(APIGatewayProxyRequestEvent().withHeader("content-type", "application/vnd.moia.v1+x-protobuf")))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ class RequestHandlerTest {
assert(response.body).isEqualTo("""{"hello":"Hello","request":""}""")
}

@Test
fun `should match request to proto handler with version accept header and return json`() {

val response = testRequestHandler.handleRequest(
APIGatewayProxyRequestEvent()
.withPath("/some-proto")
.withHttpMethod("GET")
.withHeaders(mapOf("Accept" to "application/vnd.moia.v1+json")), mockk()
)

assert(response.statusCode).isEqualTo(200)
assert(response.body).isEqualTo("""{"hello":"v1","request":""}""")
}

@Test
fun `should match request to proto handler and return proto`() {

Expand Down Expand Up @@ -109,6 +123,9 @@ class RequestHandlerTest {

defaultContentType = "application/x-protobuf"

GET("/some-proto") { _: Request<Unit> -> ResponseEntity.ok(Sample.newBuilder().setHello("v1").build()) }
.producing("application/vnd.moia.v1+x-protobuf", "application/vnd.moia.v1+json")

GET("/some-proto") { _: Request<Unit> -> ResponseEntity.ok(Sample.newBuilder().setHello("Hello").build()) }
.producing("application/x-protobuf", "application/json")
POST("/some-proto") { r: Request<Sample> -> ResponseEntity.ok(r.body) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.type.TypeFactory
import com.google.common.net.MediaType
import isCompatibleWith
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.isSubclassOf
Expand All @@ -28,9 +29,14 @@ class DeserializationHandlerChain(private val handlers: List<DeserializationHand
class JsonDeserializationHandler(private val objectMapper: ObjectMapper) : DeserializationHandler {

private val json = MediaType.parse("application/json")
private val jsonStructuredSuffixWildcard = MediaType.parse("application/*+json")

override fun supports(input: APIGatewayProxyRequestEvent) =
input.contentType() != null && MediaType.parse(input.contentType()!!).`is`(json)
if (input.contentType() == null)
false
else {
MediaType.parse(input.contentType()).let { json.isCompatibleWith(it) || jsonStructuredSuffixWildcard.isCompatibleWith(it) }
}

override fun deserialize(input: APIGatewayProxyRequestEvent, target: KType?): Any? {
val targetClass = target?.classifier as KClass<*>
Expand Down
11 changes: 11 additions & 0 deletions router/src/main/kotlin/io/moia/router/MediaTypeExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import com.google.common.net.MediaType

fun MediaType.isCompatibleWith(other: MediaType): Boolean =
if (this.`is`(other))
true
else {
type() == other.type() &&
(subtype().contains("+") && other.subtype().contains("+")) &&
this.subtype().substringBeforeLast("+") == "*" &&
this.subtype().substringAfterLast("+") == other.subtype().substringAfterLast("+")
}
5 changes: 3 additions & 2 deletions router/src/main/kotlin/io/moia/router/RequestPredicate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.moia.router

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

data class RequestPredicate(
val method: String,
Expand Down Expand Up @@ -49,7 +50,7 @@ data class RequestPredicate(
fun matchedAcceptType(acceptedMediaTypes: List<MediaType>) =
produces
.map { MediaType.parse(it) }
.firstOrNull { acceptedMediaTypes.any { acceptedType -> it.`is`(acceptedType) } }
.firstOrNull { acceptedMediaTypes.any { acceptedType -> it.isCompatibleWith(acceptedType) } }

private fun acceptMatches(acceptedMediaTypes: List<MediaType>) =
matchedAcceptType(acceptedMediaTypes) != null
Expand All @@ -58,7 +59,7 @@ data class RequestPredicate(
when {
consumes.isEmpty() -> true
contentType == null -> false
else -> consumes.any { MediaType.parse(contentType).`is`(MediaType.parse(it)) }
else -> consumes.any { MediaType.parse(contentType).isCompatibleWith(MediaType.parse(it)) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.moia.router

import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.net.MediaType
import isCompatibleWith

interface SerializationHandler {

Expand All @@ -23,8 +24,10 @@ class SerializationHandlerChain(private val handlers: List<SerializationHandler>
class JsonSerializationHandler(private val objectMapper: ObjectMapper) : SerializationHandler {

private val json = MediaType.parse("application/json")
private val jsonStructuredSuffixWildcard = MediaType.parse("application/*+json")

override fun supports(acceptHeader: MediaType, body: Any): Boolean = acceptHeader.`is`(json)
override fun supports(acceptHeader: MediaType, body: Any): Boolean =
json.isCompatibleWith(acceptHeader) || jsonStructuredSuffixWildcard.isCompatibleWith(acceptHeader)

override fun serialize(acceptHeader: MediaType, body: Any): String =
objectMapper.writeValueAsString(body)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.moia.router

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class JsonDeserializationHandlerTest {

val deserializationHandler = JsonDeserializationHandler(jacksonObjectMapper())

@Test
fun `should support json`() {
assertTrue(deserializationHandler.supports(APIGatewayProxyRequestEvent()
.withHeader("content-type", "application/json")))
assertTrue(deserializationHandler.supports(APIGatewayProxyRequestEvent()
.withHeader("content-type", "application/vnd.moia.v1+json")))
}

@Test
fun `should not support anything else than json`() {
assertFalse(deserializationHandler.supports(APIGatewayProxyRequestEvent()
.withHeader("content-type", "image/png")))
assertFalse(deserializationHandler.supports(APIGatewayProxyRequestEvent()
.withHeader("content-type", "text/plain")))
}
}
45 changes: 45 additions & 0 deletions router/src/test/kotlin/io/moia/router/MediaTypeTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.moia.router

import com.google.common.net.MediaType
import isCompatibleWith
import org.assertj.core.api.BDDAssertions.then
import org.junit.jupiter.api.Test

class MediaTypeTest {

@Test
fun `should match`() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Table Test, just a little bit, pretty please, sugar on top 😁 😁 😄

then(MediaType.parse("application/json").isCompatibleWith(MediaType.parse("application/json"))).isTrue()
}

@Test
fun `should match subtype wildcard`() {
then(MediaType.parse("application/json").isCompatibleWith(MediaType.parse("application/*"))).isTrue()
}

@Test
fun `should not match subtype wildcard in different tpye`() {
then(MediaType.parse("application/json").isCompatibleWith(MediaType.parse("image/*"))).isFalse()
}

@Test
fun `should match wildcard`() {
then(MediaType.parse("application/json").isCompatibleWith(MediaType.parse("*/*"))).isTrue()
}

@Test
fun `should match wildcard structured syntax suffix`() {
then(MediaType.parse("application/*+json").isCompatibleWith(MediaType.parse("application/vnd.moia+json"))).isTrue()
then(MediaType.parse("application/*+json").isCompatibleWith(MediaType.parse("application/vnd.moia.v1+json"))).isTrue()
}

@Test
fun `should not match wildcard structured syntax suffix on non suffix type`() {
then(MediaType.parse("application/*+json").isCompatibleWith(MediaType.parse("application/json"))).isFalse()
}

@Test
fun `should not match wildcard structured syntax suffix on differnt suffix`() {
then(MediaType.parse("application/*+json").isCompatibleWith(MediaType.parse("application/*+x-protobuf"))).isFalse()
}
}
47 changes: 45 additions & 2 deletions router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,44 @@ class RequestHandlerTest {
)

assert(response.statusCode).isEqualTo(200)
assert(response.getHeaderCaseInsensitive("content-type")).isEqualTo("application/json")
assert(response.getHeaderCaseInsensitive("content-type")).isEqualTo("application/vnd.moia.v2+json")

assert(response.body).isEqualTo("""{"greeting":"v2"}""")
}

@Test
fun `should handle subtype structured suffix wildcard`() {

val response = testRequestHandler.handleRequest(
POST("/some")
.withHeaders(mapOf(
"Accept" to "application/vnd.moia.v1+json",
"Content-Type" to "application/json"
))
.withBody("""{ "greeting": "some" }"""), mockk()
)

assert(response.statusCode).isEqualTo(200)
assert(response.body).isEqualTo("""{"greeting":"some"}""")
}

@Test
fun `should match version`() {

val response = testRequestHandler.handleRequest(
POST("/some")
.withHeaders(mapOf(
"Accept" to "application/vnd.moia.v2+json",
"Content-Type" to "application/json"
))
.withBody("""{ "greeting": "v2" }"""), mockk()
)

assert(response.statusCode).isEqualTo(200)
assert(response.body).isEqualTo("""{"greeting":"v2"}""")
assert(response.getHeaderCaseInsensitive("content-type")).isEqualTo("application/vnd.moia.v2+json")
}

@Test
fun `should fail with 406 Not Acceptable on an unparsable media type`() {

Expand Down Expand Up @@ -614,13 +647,23 @@ class RequestHandlerTest {
)
)
}

POST("/some") { r: Request<TestRequest> ->
ResponseEntity.ok(
TestResponse(
"v2"
)
)
}.producing("application/vnd.moia.v2+json")

POST("/some") { r: Request<TestRequest> ->
ResponseEntity.ok(
TestResponse(
r.body.greeting
)
)
}
}.producing("application/json", "application/*+json")

POST("/somes") { r: Request<List<TestRequest>> ->
ResponseEntity.ok(r.body.map {
TestResponse(
Expand Down