Skip to content

Commit

Permalink
fix(awslambda): extends path param normalisation to all adapters.
Browse files Browse the repository at this point in the history
  • Loading branch information
outofcoffee committed Oct 3, 2024
1 parent 096eb22 commit 58cde5f
Show file tree
Hide file tree
Showing 20 changed files with 466 additions and 154 deletions.
1 change: 0 additions & 1 deletion adapter/awslambda/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ apply plugin: 'java-library'
apply plugin: 'kotlin'

ext {
version_junit_jupiter = '5.10.0'
version_lambda_logger = '1.5.1'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import io.gatehill.imposter.util.HttpUtil
*/
class ServerV1(
responseService: ResponseService,
router: HttpRouter,
private val router: HttpRouter,
) : LambdaServer<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent>(responseService, router) {

init {
Expand All @@ -74,7 +74,7 @@ class ServerV1(
return HttpUtil.readAcceptedContentTypes(event.headers["Accept"]).contains(HttpUtil.CONTENT_TYPE_HTML)
}

override fun buildRequest(event: APIGatewayProxyRequestEvent, route: HttpRoute?) = LambdaHttpRequestV1(event, route)
override fun buildRequest(event: APIGatewayProxyRequestEvent, route: HttpRoute?) = LambdaHttpRequestV1(event, router, route)

override fun buildResponse(response: LambdaHttpResponse) = APIGatewayProxyResponseEvent().apply {
// read status again in case modified by error handler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import io.gatehill.imposter.util.HttpUtil
*/
class ServerV2(
responseService: ResponseService,
router: HttpRouter,
private val router: HttpRouter,
) : LambdaServer<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse>(responseService, router) {

init {
Expand All @@ -74,7 +74,7 @@ class ServerV2(
return HttpUtil.readAcceptedContentTypes(event.headers["Accept"]).contains(HttpUtil.CONTENT_TYPE_HTML)
}

override fun buildRequest(event: APIGatewayV2HTTPEvent, route: HttpRoute?) = LambdaHttpRequestV2(event, route)
override fun buildRequest(event: APIGatewayV2HTTPEvent, route: HttpRoute?) = LambdaHttpRequestV2(event, router, route)

override fun buildResponse(response: LambdaHttpResponse) = APIGatewayV2HTTPResponse().apply {
// read status again in case modified by error handler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import io.gatehill.imposter.awslambda.util.FormParserUtil
import io.gatehill.imposter.http.HttpMethod
import io.gatehill.imposter.http.HttpRequest
import io.gatehill.imposter.http.HttpRoute
import io.gatehill.imposter.http.HttpRouter
import io.gatehill.imposter.http.util.PathNormaliser
import io.gatehill.imposter.script.LowercaseKeysMap
import io.vertx.core.buffer.Buffer
import io.vertx.core.json.JsonObject
Expand All @@ -58,6 +60,7 @@ import io.vertx.core.json.JsonObject
*/
class LambdaHttpRequestV1(
private val event: APIGatewayProxyRequestEvent,
private val router: HttpRouter,
private val currentRoute: HttpRoute?,
) : HttpRequest {
private val baseUrl: String
Expand Down Expand Up @@ -91,10 +94,10 @@ class LambdaHttpRequestV1(
}

override val pathParams: Map<String, String>
get() = pathParameters
get() = PathNormaliser.denormaliseParams(router.normalisedParams, pathParameters)

override fun getPathParam(paramName: String): String? {
return pathParameters[paramName]
return pathParameters[PathNormaliser.getNormalisedParamName(router.normalisedParams, paramName)]
}

override val queryParams: Map<String, String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import io.gatehill.imposter.awslambda.util.FormParserUtil
import io.gatehill.imposter.http.HttpMethod
import io.gatehill.imposter.http.HttpRequest
import io.gatehill.imposter.http.HttpRoute
import io.gatehill.imposter.http.HttpRouter
import io.gatehill.imposter.http.util.PathNormaliser
import io.gatehill.imposter.script.LowercaseKeysMap
import io.vertx.core.buffer.Buffer
import io.vertx.core.json.JsonObject
Expand All @@ -58,6 +60,7 @@ import io.vertx.core.json.JsonObject
*/
class LambdaHttpRequestV2(
private val event: APIGatewayV2HTTPEvent,
private val router: HttpRouter,
private val currentRoute: HttpRoute?,
) : HttpRequest {
private val baseUrl: String
Expand Down Expand Up @@ -91,10 +94,10 @@ class LambdaHttpRequestV2(
}

override val pathParams: Map<String, String>
get() = pathParameters
get() = PathNormaliser.denormaliseParams(router.normalisedParams, pathParameters)

override fun getPathParam(paramName: String): String? {
return pathParameters[paramName]
return pathParameters[PathNormaliser.getNormalisedParamName(router.normalisedParams, paramName)]
}

override val queryParams: Map<String, String>
Expand Down
4 changes: 0 additions & 4 deletions adapter/vertxweb/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'maven-publish'

ext {
version_junit_jupiter = '5.10.0'
}

compileJava {
sourceCompatibility = JavaVersion.VERSION_11
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,22 +134,21 @@ class VertxWebServerFactoryImpl : ServerFactory {
}

private fun convertRouterToVertx(router: HttpRouter) = Router.router(router.vertx).also { vertxRouter ->
val normalisedParams = mutableMapOf<String, String>()
router.routes.forEach { httpRoute ->
val vertxRoute = httpRoute.regex?.let { regex ->
httpRoute.method?.let { method -> vertxRouter.routeWithRegex(method.convertMethodToVertx(), regex) }
?: vertxRouter.routeWithRegex(regex)

} ?: httpRoute.path?.let { path ->
val normalisedPath = VertxResourceUtil.normalisePath(normalisedParams, path)
httpRoute.method?.let { method -> vertxRouter.route(method.convertMethodToVertx(), normalisedPath) }
?: vertxRouter.route(normalisedPath)
val convertedPath = VertxResourceUtil.convertPath(path)
httpRoute.method?.let { method -> vertxRouter.route(method.convertMethodToVertx(), convertedPath) }
?: vertxRouter.route(convertedPath)

} ?: vertxRouter.route()

val handler = httpRoute.handler ?: throw IllegalStateException("No route handler set for: $httpRoute")
vertxRoute.handler { rc ->
val exchange = VertxHttpExchange(router, normalisedParams, rc, httpRoute)
val exchange = VertxHttpExchange(router, httpRoute, rc)

// don't block the event loop
handler(exchange).handle { _, t ->
Expand All @@ -161,7 +160,7 @@ class VertxWebServerFactoryImpl : ServerFactory {

router.errorHandlers.forEach { (statusCode, errorHandler) ->
vertxRouter.errorHandler(statusCode) { rc ->
errorHandler(VertxHttpExchange(router, normalisedParams, rc, null))
errorHandler(VertxHttpExchange(router, null, rc))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,13 @@ import io.vertx.ext.web.impl.ParsableMIMEValue
*/
class VertxHttpExchange(
private val router: HttpRouter,
normalisedParams: Map<String, String>,
internal val routingContext: RoutingContext,
override val currentRoute: HttpRoute?,
internal val routingContext: RoutingContext,
) : HttpExchange {
override var phase = ExchangePhase.REQUEST_RECEIVED

override val request: HttpRequest by lazy {
VertxHttpRequest(normalisedParams, routingContext)
VertxHttpRequest(router, routingContext)
}

override val response: HttpResponse by lazy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ package io.gatehill.imposter.server.vertxweb.impl

import io.gatehill.imposter.http.HttpMethod
import io.gatehill.imposter.http.HttpRequest
import io.gatehill.imposter.server.vertxweb.util.VertxResourceUtil
import io.gatehill.imposter.http.HttpRouter
import io.gatehill.imposter.http.util.PathNormaliser
import io.gatehill.imposter.server.vertxweb.util.VertxResourceUtil.convertMethodFromVertx
import io.gatehill.imposter.util.CollectionUtil
import io.vertx.core.buffer.Buffer
Expand All @@ -55,7 +56,7 @@ import io.vertx.ext.web.RoutingContext
* @author Pete Cornish
*/
class VertxHttpRequest(
private val normalisedParams: Map<String, String>,
private val router: HttpRouter,
private val routingContext: RoutingContext,
) : HttpRequest {
private val vertxRequest = routingContext.request()
Expand All @@ -80,10 +81,10 @@ class VertxHttpRequest(
}

override val pathParams: Map<String, String>
get() = VertxResourceUtil.denormaliseParams(normalisedParams, routingContext.pathParams())
get() = PathNormaliser.denormaliseParams(router.normalisedParams, routingContext.pathParams())

override fun getPathParam(paramName: String): String? {
return routingContext.pathParam(VertxResourceUtil.getNormalisedParamName(normalisedParams, paramName))
return routingContext.pathParam(PathNormaliser.getNormalisedParamName(router.normalisedParams, paramName))
}

override val queryParams: Map<String, String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,32 +42,16 @@
*/
package io.gatehill.imposter.server.vertxweb.util

import com.google.common.base.Strings
import com.google.common.collect.BiMap
import com.google.common.collect.HashBiMap
import io.gatehill.imposter.config.util.EnvVars
import io.gatehill.imposter.http.HttpMethod
import io.gatehill.imposter.http.HttpRoute
import org.apache.logging.log4j.LogManager
import java.util.UUID

/**
* @author Pete Cornish
*/
object VertxResourceUtil {
private val LOGGER = LogManager.getLogger(VertxResourceUtil::class.java)

/**
* Vert.x documentation says:
* > The placeholders consist of : followed by the parameter name.
* > Parameter names consist of any alphabetic character, numeric character or underscore.
*
* See: https://vertx.io/docs/vertx-web/java/#_capturing_path_parameters
*
* This regex pattern does not include the colon prefix.
*/
private val VERTX_PATH_PARAM_NAME = Regex("[a-zA-Z0-9_]+")

private val METHODS: BiMap<HttpMethod, io.vertx.core.http.HttpMethod?> = HashBiMap.create()

/**
Expand Down Expand Up @@ -100,10 +84,7 @@ object VertxResourceUtil {
METHODS.inverse()[this] ?: throw UnsupportedOperationException("Unknown method: $this")

/**
* Normalises path parameters to Vert.x format.
*
* If a path parameter name does not match the Vert.x format, it is replaced with a UUID,
* and a mapping is added to the `normalisedParams` map.
* Converts path parameters to Vert.x format.
*
* For example:
* ```
Expand All @@ -113,62 +94,27 @@ object VertxResourceUtil {
* ```
* /:pathParam/notParam
* ```
*
* A path parameter name that does not match the Vert.x format, such as:
* ```
* /{param-with-dashes}
* ```
* will be converted to:
* ```
* /:123e4567e89b12d3a4564266141740000
* ```
* and the mapping `param-with-dashes -> 123e4567e89b12d3a4564266141740000`
* will be added to the `normalisedParams` map.
*
* @param normalisedParams a map to store the normalised parameter names
* @param rawPath the path to normalise
*/
fun normalisePath(normalisedParams: MutableMap<String, String>, rawPath: String): String {
fun convertPath(rawPath: String): String {
var path = rawPath
if (!Strings.isNullOrEmpty(path)) {
if (path.isNotEmpty()) {
if (escapeColonsInPath) {
path = path.replace(":", "%3A")
}

// convert to colon format
var matchFound: Boolean
do {
val matcher = HttpRoute.PATH_PARAM_PLACEHOLDER.matcher(path)
matchFound = matcher.find()
if (matchFound) {
val finalParamName = matcher.group(1).let { paramName ->
if (paramName.matches(VERTX_PATH_PARAM_NAME)) {
paramName
} else {
val existingMapping = normalisedParams.entries.find { it.value == paramName }
if (null != existingMapping) {
return@let existingMapping.key
}
val normalisedName = UUID.randomUUID().toString().replace("-", "")
normalisedParams[normalisedName] = paramName
normalisedName
}
}
path = matcher.replaceFirst(":$finalParamName")
val paramName = matcher.group(1)
path = matcher.replaceFirst(":$paramName")
}
} while (matchFound)
}
return path
}

fun getNormalisedParamName(normalisedParams: Map<String, String>, originalParamName: String): String {
if (originalParamName.matches(VERTX_PATH_PARAM_NAME)) {
return originalParamName
}
return normalisedParams.entries.find { it.value == originalParamName }?.key ?: originalParamName
}

fun denormaliseParams(normalisedParams: Map<String, String>, vertxParams: Map<String, String>): Map<String, String> {
// if it's not in the map it doesn't need to be denormalised
return vertxParams.mapKeys { normalisedParams[it.key] ?: it.key }
return path
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@
package io.gatehill.imposter.server.vertxweb.util

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test

/**
Expand All @@ -57,57 +54,19 @@ import org.junit.jupiter.api.Test
class VertxResourceUtilTest {
@Test
fun `should convert path to Vertx format`() {
val normalisedParams = mutableMapOf<String, String>()
val result = VertxResourceUtil.normalisePath(normalisedParams, "/{pathParam}/notParam")

assertEquals(0, normalisedParams.size)
val result = VertxResourceUtil.convertPath("/{pathParam}/notParam")
assertEquals("/:pathParam/notParam", result)
}

@Test
fun `should handle param and plain string`() {
val normalisedParams = mutableMapOf<String, String>()
val result = VertxResourceUtil.normalisePath(normalisedParams, "/{firstParam}.notParam")

assertEquals(0, normalisedParams.size)
val result = VertxResourceUtil.convertPath("/{firstParam}.notParam")
assertEquals("/:firstParam.notParam", result)
}

@Test
fun `should normalise path`() {
val normalisedParams = mutableMapOf<String, String>()
val result = VertxResourceUtil.normalisePath(normalisedParams, "/{firstParam}/{second-param}/notParam")

assertEquals(1, normalisedParams.size)
assertFalse(normalisedParams.containsKey("firstParam"))

val secondParam = normalisedParams.entries.first()
assertNotNull(secondParam)
assertEquals("second-param", secondParam.value)
assertNotEquals("second-param", secondParam.key)
assertEquals("/:firstParam/:${secondParam.key}/notParam", result)
}

@Test
fun `should get normalised param name`() {
val normalisedParams = mapOf(
"abcdef1234567890" to "kebab-param"
)
assertEquals("abcdef1234567890", VertxResourceUtil.getNormalisedParamName(normalisedParams, "kebab-param"))
assertEquals("normalParam", VertxResourceUtil.getNormalisedParamName(normalisedParams, "normalParam"))
}

@Test
fun `should denormalise params`() {
val normalisedParams = mapOf(
"abcdef1234567890" to "kebab-param"
)
val vertxParams = mapOf(
"abcdef1234567890" to "param value",
"content-type" to "application/json"
)
val result = VertxResourceUtil.denormaliseParams(normalisedParams, vertxParams)
assertEquals("param value", result["kebab-param"])
assertEquals("application/json", result["content-type"])
fun `should convert path with multiple path params`() {
val result = VertxResourceUtil.convertPath("/{firstParam}/{secondParam}/notParam")
assertEquals("/:firstParam/:secondParam/notParam", result)
}
}
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ ext {
version_jackson_databind = '2.13.4.2'
version_jaxb_api = '2.3.1'
version_junit = '4.13.2'
version_junit_jupiter = '5.10.0'
version_log4j = '2.18.0'
version_micrometer = '1.9.4'
version_mockito = '5.2.0'
Expand Down
Loading

0 comments on commit 58cde5f

Please sign in to comment.