diff --git a/.java-version b/.java-version new file mode 100644 index 0000000..03b6389 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +17.0 diff --git a/build.gradle.kts b/build.gradle.kts index cad5a1f..a554e58 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,11 +10,11 @@ buildscript { plugins { java - kotlin("jvm") version "2.0.0" + kotlin("jvm") version "2.1.0" `maven-publish` jacoco id("com.github.kt3k.coveralls") version "2.12.2" - id("org.jmailen.kotlinter") version "4.3.0" + id("org.jmailen.kotlinter") version "5.0.1" } group = "com.github.moia-dev" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..18090eb --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +java = "17" diff --git a/router-openapi-request-validator/build.gradle.kts b/router-openapi-request-validator/build.gradle.kts index c668883..4e2cdfb 100644 --- a/router-openapi-request-validator/build.gradle.kts +++ b/router-openapi-request-validator/build.gradle.kts @@ -6,11 +6,11 @@ dependencies { implementation(kotlin("stdlib")) implementation(kotlin("reflect")) - api("com.atlassian.oai:swagger-request-validator-core:2.42.0") + api("com.atlassian.oai:swagger-request-validator-core:2.44.1") api(project(":router")) - testImplementation("org.junit.jupiter:junit-jupiter-engine:5.11.1") - testImplementation("org.assertj:assertj-core:3.26.0") - testImplementation("io.mockk:mockk:1.13.11") - testImplementation("org.slf4j:slf4j-simple:2.0.13") + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.11.4") + testImplementation("org.assertj:assertj-core:3.27.2") + testImplementation("io.mockk:mockk:1.13.14") + testImplementation("org.slf4j:slf4j-simple:2.0.16") } \ No newline at end of file diff --git a/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/OpenApiValidator.kt b/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/OpenApiValidator.kt index 9e6ce1f..9362204 100644 --- a/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/OpenApiValidator.kt +++ b/router-openapi-request-validator/src/main/kotlin/io/moia/router/openapi/OpenApiValidator.kt @@ -10,30 +10,30 @@ import com.atlassian.oai.validator.model.SimpleResponse import com.atlassian.oai.validator.report.ValidationReport import org.slf4j.LoggerFactory -class OpenApiValidator(val specUrlOrPayload: String) { +class OpenApiValidator( + val specUrlOrPayload: String, +) { val validator = OpenApiInteractionValidator.createFor(specUrlOrPayload).build() fun validate( request: APIGatewayProxyRequestEvent, response: APIGatewayProxyResponseEvent, - ): ValidationReport { - return validator.validate(request.toRequest(), response.toResponse()) + ): ValidationReport = + validator + .validate(request.toRequest(), response.toResponse()) .also { if (it.hasErrors()) log.error("error validating request and response against $specUrlOrPayload - $it") } - } fun assertValid( request: APIGatewayProxyRequestEvent, response: APIGatewayProxyResponseEvent, - ) { - return validate(request, response).let { - if (it.hasErrors()) { - throw ApiInteractionInvalid( - specUrlOrPayload, - request, - response, - it, - ) - } + ) = validate(request, response).let { + if (it.hasErrors()) { + throw ApiInteractionInvalid( + specUrlOrPayload, + request, + response, + it, + ) } } @@ -72,7 +72,7 @@ class OpenApiValidator(val specUrlOrPayload: String) { private fun APIGatewayProxyRequestEvent.toRequest(): Request { val builder = - when (httpMethod.toLowerCase()) { + when (httpMethod.lowercase()) { "get" -> SimpleRequest.Builder.get(path) "post" -> SimpleRequest.Builder.post(path) "put" -> SimpleRequest.Builder.put(path) @@ -88,13 +88,12 @@ class OpenApiValidator(val specUrlOrPayload: String) { return builder.build() } - private fun APIGatewayProxyResponseEvent.toResponse(): Response { - return SimpleResponse.Builder + private fun APIGatewayProxyResponseEvent.toResponse(): Response = + SimpleResponse.Builder .status(statusCode) .withBody(body) .also { headers.forEach { h -> it.withHeader(h.key, h.value) } } .build() - } companion object { val log = LoggerFactory.getLogger(OpenApiValidator::class.java) diff --git a/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/OpenApiValidatorTest.kt b/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/OpenApiValidatorTest.kt index da0252f..6e3f817 100644 --- a/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/OpenApiValidatorTest.kt +++ b/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/OpenApiValidatorTest.kt @@ -53,7 +53,9 @@ class OpenApiValidatorTest { } class TestRequestHandler : RequestHandler() { - data class TestResponse(val name: String) + data class TestResponse( + val name: String, + ) override val router = Router.router { @@ -67,7 +69,9 @@ class OpenApiValidatorTest { } class TestInvalidRequestHandler : RequestHandler() { - data class TestResponseInvalid(val invalid: String) + data class TestResponseInvalid( + val invalid: String, + ) override val router = Router.router { diff --git a/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapperTest.kt b/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapperTest.kt index db8a752..006a39c 100644 --- a/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapperTest.kt +++ b/router-openapi-request-validator/src/test/kotlin/io/moia/router/openapi/ValidatingRequestRouterWrapperTest.kt @@ -26,8 +26,7 @@ class ValidatingRequestRouterWrapperTest { thenThrownBy { ValidatingRequestRouterWrapper(InvalidTestRequestHandler(), "openapi.yml") .handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk()) - } - .isInstanceOf(OpenApiValidator.ApiInteractionInvalid::class.java) + }.isInstanceOf(OpenApiValidator.ApiInteractionInvalid::class.java) .hasMessageContaining("Response status 404 not defined for path") } @@ -36,8 +35,7 @@ class ValidatingRequestRouterWrapperTest { thenThrownBy { ValidatingRequestRouterWrapper(InvalidTestRequestHandler(), "openapi.yml") .handleRequest(GET("/path-not-documented").withAcceptHeader("application/json"), mockk()) - } - .isInstanceOf(OpenApiValidator.ApiInteractionInvalid::class.java) + }.isInstanceOf(OpenApiValidator.ApiInteractionInvalid::class.java) .hasMessageContaining("No API path found that matches request") } @@ -59,10 +57,8 @@ class ValidatingRequestRouterWrapperTest { delegate = OpenApiValidatorTest.TestRequestHandler(), specUrlOrPayload = "openapi.yml", additionalRequestValidationFunctions = listOf({ _ -> throw RequestValidationFailedException() }), - ) - .handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk()) - } - .isInstanceOf(RequestValidationFailedException::class.java) + ).handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk()) + }.isInstanceOf(RequestValidationFailedException::class.java) } @Test @@ -72,10 +68,8 @@ class ValidatingRequestRouterWrapperTest { delegate = OpenApiValidatorTest.TestRequestHandler(), specUrlOrPayload = "openapi.yml", additionalResponseValidationFunctions = listOf({ _, _ -> throw ResponseValidationFailedException() }), - ) - .handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk()) - } - .isInstanceOf(ResponseValidationFailedException::class.java) + ).handleRequest(GET("/tests").withAcceptHeader("application/json"), mockk()) + }.isInstanceOf(ResponseValidationFailedException::class.java) } private class RequestValidationFailedException : RuntimeException("request validation failed") diff --git a/router-protobuf/build.gradle.kts b/router-protobuf/build.gradle.kts index 8c79fc5..7fab535 100644 --- a/router-protobuf/build.gradle.kts +++ b/router-protobuf/build.gradle.kts @@ -6,23 +6,23 @@ repositories { mavenCentral() } -val protoVersion = "4.27.2" +val protoVersion = "4.29.3" dependencies { implementation(kotlin("stdlib")) implementation(kotlin("reflect")) - implementation("org.slf4j:slf4j-api:2.0.13") + implementation("org.slf4j:slf4j-api:2.0.16") api("com.google.protobuf:protobuf-java:$protoVersion") api("com.google.protobuf:protobuf-java-util:$protoVersion") - implementation("com.google.guava:guava:33.2.1-jre") + implementation("com.google.guava:guava:33.4.0-jre") api(project(":router")) - testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.2") + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.11.4") testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1") - testImplementation("org.assertj:assertj-core:3.26.0") - testImplementation("io.mockk:mockk:1.13.11") - testImplementation("org.slf4j:slf4j-simple:2.0.13") + testImplementation("org.assertj:assertj-core:3.27.2") + testImplementation("io.mockk:mockk:1.13.14") + testImplementation("org.slf4j:slf4j-simple:2.0.16") testImplementation("com.jayway.jsonpath:json-path:2.9.0") } diff --git a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoBufUtils.kt b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoBufUtils.kt index beafb8e..9b0b628 100644 --- a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoBufUtils.kt +++ b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoBufUtils.kt @@ -9,15 +9,19 @@ import com.google.protobuf.util.JsonFormat object ProtoBufUtils { fun toJsonWithoutWrappers(proto: GeneratedMessage): String { - val message = JsonFormat.printer().omittingInsignificantWhitespace().alwaysPrintFieldsWithNoPresence().print(proto) + val message = + JsonFormat + .printer() + .omittingInsignificantWhitespace() + .alwaysPrintFieldsWithNoPresence() + .print(proto) return removeWrapperObjects(message) } - fun removeWrapperObjects(json: String): String { - return removeWrapperObjects( + fun removeWrapperObjects(json: String): String = + removeWrapperObjects( jacksonObjectMapper().readTree(json), ).toString() - } fun removeWrapperObjects(json: JsonNode): JsonNode { if (json.isArray) { diff --git a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoEnabledRequestHandler.kt b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoEnabledRequestHandler.kt index 56c3834..b4208c2 100644 --- a/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoEnabledRequestHandler.kt +++ b/router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoEnabledRequestHandler.kt @@ -13,7 +13,5 @@ abstract class ProtoEnabledRequestHandler : RequestHandler() { override fun createResponse( contentType: MediaType, response: ResponseEntity, - ): APIGatewayProxyResponseEvent { - return super.createResponse(contentType, response).withIsBase64Encoded(true) - } + ): APIGatewayProxyResponseEvent = super.createResponse(contentType, response).withIsBase64Encoded(true) } diff --git a/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoBufUtilsTest.kt b/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoBufUtilsTest.kt index 4b60734..8ea7833 100644 --- a/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoBufUtilsTest.kt +++ b/router-protobuf/src/test/kotlin/io/moia/router/proto/ProtoBufUtilsTest.kt @@ -11,7 +11,8 @@ class ProtoBufUtilsTest { @Test fun `should serialize empty list`() { val message = - ComplexSample.newBuilder() + ComplexSample + .newBuilder() .addAllSamples(emptyList()) .build() @@ -23,7 +24,8 @@ class ProtoBufUtilsTest { @Test fun `should remove wrapper object`() { val message = - ComplexSample.newBuilder() + ComplexSample + .newBuilder() .setSomeString(StringValue.newBuilder().setValue("some").build()) .build() @@ -35,7 +37,8 @@ class ProtoBufUtilsTest { @Test fun `should serialize value when it is the default`() { val message = - ComplexSample.newBuilder() + ComplexSample + .newBuilder() .setEnumAttribute(ONE) // enum zero value .build() diff --git a/router-protobuf/src/test/kotlin/io/moia/router/proto/RequestHandlerTest.kt b/router-protobuf/src/test/kotlin/io/moia/router/proto/RequestHandlerTest.kt index 96d5c36..d2ed082 100644 --- a/router-protobuf/src/test/kotlin/io/moia/router/proto/RequestHandlerTest.kt +++ b/router-protobuf/src/test/kotlin/io/moia/router/proto/RequestHandlerTest.kt @@ -62,12 +62,23 @@ class RequestHandlerTest { ) assertThat(response.statusCode).isEqualTo(200) - assertThat(Sample.parseFrom(response.bodyAsBytes())).isEqualTo(Sample.newBuilder().setHello("Hello").setRequest("").build()) + assertThat(Sample.parseFrom(response.bodyAsBytes())).isEqualTo( + Sample + .newBuilder() + .setHello("Hello") + .setRequest("") + .build(), + ) } @Test fun `should match request to proto handler and deserialize and return proto`() { - val request = Sample.newBuilder().setHello("Hello").setRequest("").build() + val request = + Sample + .newBuilder() + .setHello("Hello") + .setRequest("") + .build() val response = testRequestHandler.handleRequest( @@ -104,7 +115,9 @@ class RequestHandlerTest { assertThat(response.statusCode).isEqualTo(406) assertThat( - io.moia.router.proto.sample.SampleOuterClass.ApiError.parseFrom(response.bodyAsBytes()).getCode(), + io.moia.router.proto.sample.SampleOuterClass.ApiError + .parseFrom(response.bodyAsBytes()) + .getCode(), ).isEqualTo("NOT_ACCEPTABLE") } @@ -122,7 +135,10 @@ class RequestHandlerTest { ) assertThat(response.statusCode).isEqualTo(400) - with(io.moia.router.proto.sample.SampleOuterClass.ApiError.parseFrom(response.bodyAsBytes())) { + with( + io.moia.router.proto.sample.SampleOuterClass.ApiError + .parseFrom(response.bodyAsBytes()), + ) { assertThat(getCode()).isEqualTo("BOOM") assertThat(getMessage()).isEqualTo("boom") } @@ -146,14 +162,16 @@ class RequestHandlerTest { } override fun createErrorBody(error: ApiError): Any = - io.moia.router.proto.sample.SampleOuterClass.ApiError.newBuilder() + io.moia.router.proto.sample.SampleOuterClass.ApiError + .newBuilder() .setMessage(error.message) .setCode(error.code) .build() override fun createUnprocessableEntityErrorBody(errors: List): Any = errors.map { error -> - io.moia.router.proto.sample.SampleOuterClass.UnprocessableEntityError.newBuilder() + io.moia.router.proto.sample.SampleOuterClass.UnprocessableEntityError + .newBuilder() .setMessage(error.message) .setCode(error.code) .setPath(error.path) diff --git a/router/build.gradle.kts b/router/build.gradle.kts index 84221ad..ded63b8 100644 --- a/router/build.gradle.kts +++ b/router/build.gradle.kts @@ -4,17 +4,17 @@ dependencies { implementation(kotlin("stdlib")) implementation(kotlin("reflect")) api("com.amazonaws:aws-lambda-java-core:1.2.3") - api("com.amazonaws:aws-lambda-java-events:3.11.5") + api("com.amazonaws:aws-lambda-java-events:3.14.0") - implementation("org.slf4j:slf4j-api:2.0.13") - api("com.fasterxml.jackson.core:jackson-databind:2.17.1") - api("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.1") - api("com.google.guava:guava:33.2.1-jre") + implementation("org.slf4j:slf4j-api:2.0.16") + api("com.fasterxml.jackson.core:jackson-databind:2.18.2") + api("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2") + api("com.google.guava:guava:33.4.0-jre") - testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.2") - testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2") + testImplementation("org.junit.jupiter:junit-jupiter-engine:5.11.4") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.11.4") testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1") - testImplementation("org.assertj:assertj-core:3.26.0") - testImplementation("io.mockk:mockk:1.13.11") - testImplementation("ch.qos.logback:logback-classic:1.5.6") + testImplementation("org.assertj:assertj-core:3.27.2") + testImplementation("io.mockk:mockk:1.13.14") + testImplementation("ch.qos.logback:logback-classic:1.5.16") } diff --git a/router/src/main/kotlin/io/moia/router/APIGatewayProxyEventExtensions.kt b/router/src/main/kotlin/io/moia/router/APIGatewayProxyEventExtensions.kt index 148d708..f30ccee 100644 --- a/router/src/main/kotlin/io/moia/router/APIGatewayProxyEventExtensions.kt +++ b/router/src/main/kotlin/io/moia/router/APIGatewayProxyEventExtensions.kt @@ -23,7 +23,10 @@ import java.net.URI import java.util.Base64 /** Data class that represents an HTTP header */ -data class Header(val name: String, val value: String) +data class Header( + val name: String, + val value: String, +) fun APIGatewayProxyRequestEvent.acceptHeader() = getHeaderCaseInsensitive("accept") @@ -120,7 +123,8 @@ private fun getCaseInsensitive( key: String, map: Map?, ): String? = - map?.entries + map + ?.entries ?.firstOrNull { key.equals(it.key, ignoreCase = true) } ?.value diff --git a/router/src/main/kotlin/io/moia/router/ApiException.kt b/router/src/main/kotlin/io/moia/router/ApiException.kt index 60f6ecc..544289b 100644 --- a/router/src/main/kotlin/io/moia/router/ApiException.kt +++ b/router/src/main/kotlin/io/moia/router/ApiException.kt @@ -23,9 +23,8 @@ open class ApiException( val details: Map = emptyMap(), cause: Throwable? = null, ) : RuntimeException(message, cause) { - override fun toString(): String { - return "ApiException(message='$message', code='$code', httpResponseStatus=$httpResponseStatus, details=$details, cause=$cause)" - } + override fun toString(): String = + "ApiException(message='$message', code='$code', httpResponseStatus=$httpResponseStatus, details=$details, cause=$cause)" fun toApiError() = ApiError(super.message!!, code, details) diff --git a/router/src/main/kotlin/io/moia/router/DeserializationHandler.kt b/router/src/main/kotlin/io/moia/router/DeserializationHandler.kt index 7a4e12b..99fb5b2 100644 --- a/router/src/main/kotlin/io/moia/router/DeserializationHandler.kt +++ b/router/src/main/kotlin/io/moia/router/DeserializationHandler.kt @@ -34,8 +34,9 @@ interface DeserializationHandler { ): Any? } -class DeserializationHandlerChain(private val handlers: List) : - DeserializationHandler { +class DeserializationHandlerChain( + private val handlers: List, +) : DeserializationHandler { override fun supports(input: APIGatewayProxyRequestEvent): Boolean = handlers.any { it.supports(input) } override fun deserialize( @@ -44,7 +45,9 @@ class DeserializationHandlerChain(private val handlers: List Unit targetClass == String::class -> input.body!! targetClass.isSubclassOf(Collection::class) -> { - val kClass = target.arguments.first().type!!.classifier as KClass<*> + val kClass = + target.arguments + .first() + .type!! + .classifier as KClass<*> val type = - TypeFactory.defaultInstance() + TypeFactory + .defaultInstance() .constructParametricType(targetClass.javaObjectType, kClass.javaObjectType) objectMapper.readValue(input.body, type) } diff --git a/router/src/main/kotlin/io/moia/router/MediaTypeExtensions.kt b/router/src/main/kotlin/io/moia/router/MediaTypeExtensions.kt index 8f538e5..429007e 100644 --- a/router/src/main/kotlin/io/moia/router/MediaTypeExtensions.kt +++ b/router/src/main/kotlin/io/moia/router/MediaTypeExtensions.kt @@ -20,8 +20,14 @@ 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("+") && (other.parameters().isEmpty || this.parameters() == other.parameters()) + type() == other.type() && + (subtype().contains("+") && other.subtype().contains("+")) && + this + .subtype() + .substringBeforeLast("+") == "*" && + this.subtype().substringAfterLast("+") == + other + .subtype() + .substringAfterLast("+") && + (other.parameters().isEmpty || this.parameters() == other.parameters()) } diff --git a/router/src/main/kotlin/io/moia/router/PermissionHandler.kt b/router/src/main/kotlin/io/moia/router/PermissionHandler.kt index 202dfc9..eea617f 100644 --- a/router/src/main/kotlin/io/moia/router/PermissionHandler.kt +++ b/router/src/main/kotlin/io/moia/router/PermissionHandler.kt @@ -41,7 +41,11 @@ open class JwtAccessor( fun extractJwtToken(): String? = // support "Bearer " as well as "" - request.getHeaderCaseInsensitive(authorizationHeaderName)?.split(" ")?.toList()?.last() + request + .getHeaderCaseInsensitive(authorizationHeaderName) + ?.split(" ") + ?.toList() + ?.last() fun extractJwtClaims() = extractJwtToken() @@ -54,8 +58,7 @@ open class JwtAccessor( } catch (e: Exception) { return null } - } - ?.let { objectMapper.readValue>(it) } + }?.let { objectMapper.readValue>(it) } } open class JwtPermissionHandler( @@ -77,7 +80,8 @@ open class JwtPermissionHandler( } internal open fun extractPermissions(): Set = - accessor.extractJwtClaims() + accessor + .extractJwtClaims() ?.let { it[permissionsClaim] } ?.let { when (it) { diff --git a/router/src/main/kotlin/io/moia/router/RequestHandler.kt b/router/src/main/kotlin/io/moia/router/RequestHandler.kt index 093b59a..b1e4be1 100644 --- a/router/src/main/kotlin/io/moia/router/RequestHandler.kt +++ b/router/src/main/kotlin/io/moia/router/RequestHandler.kt @@ -88,7 +88,8 @@ abstract class RequestHandler : RequestHandler - e.toResponseEntity(this::createErrorBody) + e + .toResponseEntity(this::createErrorBody) .also { logApiException(e, input) } else -> exceptionToResponseEntity(e) @@ -143,15 +144,14 @@ abstract class RequestHandler : RequestHandler, input: APIGatewayProxyRequestEvent, - ): Any? { - return when { + ): Any? = + when { handler.requestType.classifier as KClass<*> == Unit::class -> Unit input.body == null && handler.requestType.isMarkedNullable -> null input.body == null -> throw ApiException("no request body present", "REQUEST_BODY_MISSING", 400) input.body is String && handler.requestType.classifier as KClass<*> == String::class -> input.body else -> deserializationHandlerChain.deserialize(input, handler.requestType) } - } private fun handleNonDirectMatch( defaultContentType: MediaType, @@ -234,7 +234,11 @@ abstract class RequestHandler : RequestHandler, Set) -> RequestPredicate @Suppress("FunctionName") -class Router(private val predicateFactory: PredicateFactory) { +class Router( + private val predicateFactory: PredicateFactory, +) { val routes = mutableListOf>() var defaultConsuming = setOf("application/json") @@ -133,12 +135,14 @@ class RouterFunction( val requestPredicate: RequestPredicate, val handler: HandlerFunctionWrapper, ) { - override fun toString(): String { - return "RouterFunction(requestPredicate=$requestPredicate)" - } + override fun toString(): String = "RouterFunction(requestPredicate=$requestPredicate)" } -data class Request(val apiRequest: APIGatewayProxyRequestEvent, val body: I, val pathPattern: String = apiRequest.path) { +data class Request( + val apiRequest: APIGatewayProxyRequestEvent, + val body: I, + val pathPattern: String = apiRequest.path, +) { val pathParameters by lazy { UriTemplate.from(pathPattern).extract(apiRequest.path) } val queryParameters: Map? by lazy { apiRequest.queryStringParameters } val multiValueQueryStringParameters: Map>? by lazy { apiRequest.multiValueQueryStringParameters } diff --git a/router/src/main/kotlin/io/moia/router/SerializationHandler.kt b/router/src/main/kotlin/io/moia/router/SerializationHandler.kt index 8941358..35a3722 100644 --- a/router/src/main/kotlin/io/moia/router/SerializationHandler.kt +++ b/router/src/main/kotlin/io/moia/router/SerializationHandler.kt @@ -32,8 +32,9 @@ interface SerializationHandler { ): String } -class SerializationHandlerChain(private val handlers: List) : - SerializationHandler { +class SerializationHandlerChain( + private val handlers: List, +) : SerializationHandler { override fun supports( acceptHeader: MediaType, body: Any, @@ -45,7 +46,9 @@ class SerializationHandlerChain(private val handlers: List ): String = handlers.first { it.supports(acceptHeader, body) }.serialize(acceptHeader, body) } -class JsonSerializationHandler(private val objectMapper: ObjectMapper) : SerializationHandler { +class JsonSerializationHandler( + private val objectMapper: ObjectMapper, +) : SerializationHandler { private val json = MediaType.parse("application/json") private val jsonStructuredSuffixWildcard = MediaType.parse("application/*+json") @@ -60,7 +63,9 @@ class JsonSerializationHandler(private val objectMapper: ObjectMapper) : Seriali ): String = objectMapper.writeValueAsString(body) } -class PlainTextSerializationHandler(val supportedAcceptTypes: List = listOf(MediaType.parse("text/*"))) : SerializationHandler { +class PlainTextSerializationHandler( + val supportedAcceptTypes: List = listOf(MediaType.parse("text/*")), +) : SerializationHandler { override fun supports( acceptHeader: MediaType, body: Any, diff --git a/router/src/main/kotlin/io/moia/router/UriTemplate.kt b/router/src/main/kotlin/io/moia/router/UriTemplate.kt index b7c5bc8..69ed53d 100644 --- a/router/src/main/kotlin/io/moia/router/UriTemplate.kt +++ b/router/src/main/kotlin/io/moia/router/UriTemplate.kt @@ -19,7 +19,9 @@ package io.moia.router import java.net.URLDecoder import java.util.regex.Pattern -class UriTemplate private constructor(private val template: String) { +class UriTemplate private constructor( + private val template: String, +) { private val templateRegex: Regex private val matches: Sequence private val parameterNames: List @@ -31,17 +33,18 @@ class UriTemplate private constructor(private val template: String) { matches = PATH_VARIABLE_REGEX.findAll(template) parameterNames = matches.map { it.groupValues[1] }.toList() templateRegex = - template.replace( - PATH_VARIABLE_REGEX, - { notMatched -> Pattern.quote(notMatched) }, - { matched -> - // check for greedy path variables, e.g. '{proxy+}' - if (matched.groupValues[1].endsWith("+")) { - return@replace "(.+)" - } - if (matched.groupValues[2].isBlank()) "([^/]+)" else "(${matched.groupValues[2]})" - }, - ).toRegex() + template + .replace( + PATH_VARIABLE_REGEX, + { notMatched -> Pattern.quote(notMatched) }, + { matched -> + // check for greedy path variables, e.g. '{proxy+}' + if (matched.groupValues[1].endsWith("+")) { + return@replace "(.+)" + } + if (matched.groupValues[2].isBlank()) "([^/]+)" else "(${matched.groupValues[2]})" + }, + ).toRegex() } companion object { @@ -59,7 +62,11 @@ class UriTemplate private constructor(private val template: String) { fun extract(uri: String): Map = parameterNames.zip(templateRegex.findParameterValues(uri.trimSlashes())).toMap() private fun Regex.findParameterValues(uri: String): List = - findAll(uri).first().groupValues.drop(1).map { URLDecoder.decode(it, "UTF-8") } + findAll(uri) + .first() + .groupValues + .drop(1) + .map { URLDecoder.decode(it, "UTF-8") } private fun String.replace( regex: Regex, diff --git a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt index 7c0e5ec..e0f1bd3 100644 --- a/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt +++ b/router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt @@ -108,8 +108,7 @@ class RequestHandlerTest { "Accept" to "application/json", "Content-Type" to "application/json", ), - ) - .withBody("""{ "greeting": "some" }"""), + ).withBody("""{ "greeting": "some" }"""), mockk(), ) @@ -127,8 +126,7 @@ class RequestHandlerTest { "Accept" to "application/json", "Content-Type" to "application/json", ), - ) - .withBody("""[{ "greeting": "some" },{ "greeting": "some1" }]""".trimMargin()), + ).withBody("""[{ "greeting": "some" },{ "greeting": "some1" }]""".trimMargin()), mockk(), ) @@ -232,8 +230,7 @@ class RequestHandlerTest { "Accept" to "application/json", "Content-Type" to "application/json", ), - ) - .withBody("{}"), + ).withBody("{}"), mockk(), ) assertThat(response.statusCode).isEqualTo(422) @@ -249,8 +246,7 @@ class RequestHandlerTest { "Accept" to "application/json", "Content-Type" to "application/json", ), - ) - .withBody("""{"greeting": "hello","age": "a"}"""), + ).withBody("""{"greeting": "hello","age": "a"}"""), mockk(), ) assertThat(response.statusCode).isEqualTo(422) @@ -274,8 +270,7 @@ class RequestHandlerTest { "Accept" to "application/json", "Content-Type" to "application/json", ), - ) - .withBody("""{"greeting": "hello","age": 1, "bday": "2000-01-AA"}"""), + ).withBody("""{"greeting": "hello","age": 1, "bday": "2000-01-AA"}"""), mockk(), ) assertThat(response.statusCode).isEqualTo(422) @@ -299,8 +294,7 @@ class RequestHandlerTest { "Accept" to "application/json", "Content-Type" to "application/json", ), - ) - .withBody("""{"greeting": "hello", bday: "2000-01-01"}"""), + ).withBody("""{"greeting": "hello", bday: "2000-01-01"}"""), mockk(), ) assertThat(response.statusCode).isEqualTo(422) @@ -324,8 +318,7 @@ class RequestHandlerTest { "Accept" to "application/json", "Content-Type" to "application/json", ), - ) - .withBody(null), + ).withBody(null), mockk(), ) assertThat(response.statusCode).isEqualTo(400) @@ -342,8 +335,7 @@ class RequestHandlerTest { "Accept" to "application/json", "Content-Type" to "application/json", ), - ) - .withBody(null), + ).withBody(null), mockk(), ) assertThat(response.statusCode).isEqualTo(200) @@ -388,8 +380,7 @@ class RequestHandlerTest { "Accept" to "application/xhtml+xml, application/json, application/xml;q=0.9, image/webp, */*;q=0.8", "Content-Type" to "application/json", ), - ) - .withBody("""{ "greeting": "some" }"""), + ).withBody("""{ "greeting": "some" }"""), mockk(), ) @@ -409,8 +400,7 @@ class RequestHandlerTest { "Accept" to "*/*", "Content-Type" to "application/json", ), - ) - .withBody("""{ "greeting": "some" }"""), + ).withBody("""{ "greeting": "some" }"""), mockk(), ) @@ -430,8 +420,7 @@ class RequestHandlerTest { "Accept" to "application/vnd.moia.v1+json", "Content-Type" to "application/json", ), - ) - .withBody("""{ "greeting": "some" }"""), + ).withBody("""{ "greeting": "some" }"""), mockk(), ) @@ -449,8 +438,7 @@ class RequestHandlerTest { "Accept" to "application/vnd.moia.v2+json", "Content-Type" to "application/json", ), - ) - .withBody("""{ "greeting": "v2" }"""), + ).withBody("""{ "greeting": "v2" }"""), mockk(), ) @@ -469,8 +457,7 @@ class RequestHandlerTest { "Accept" to "*", "Content-Type" to "application/json", ), - ) - .withBody("""{ "greeting": "some" }"""), + ).withBody("""{ "greeting": "some" }"""), mockk(), ) @@ -485,7 +472,8 @@ class RequestHandlerTest { .withHeaders( mapOf( "Accept" to "application/json", - "Authorization" to "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24xIn0.E3PxWx68uP2s9yyAV7UVs8egyrGTIuWXjtkcqAA840I", + "Authorization" to + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24xIn0.E3PxWx68uP2s9yyAV7UVs8egyrGTIuWXjtkcqAA840I", ), ), mockk(), @@ -502,7 +490,8 @@ class RequestHandlerTest { .withHeaders( mapOf( "Accept" to "application/json", - "Custom-Auth" to "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24xIn0.E3PxWx68uP2s9yyAV7UVs8egyrGTIuWXjtkcqAA840I", + "Custom-Auth" to + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24xIn0.E3PxWx68uP2s9yyAV7UVs8egyrGTIuWXjtkcqAA840I", ), ), mockk(), @@ -519,7 +508,8 @@ class RequestHandlerTest { .withHeaders( mapOf( "Accept" to "application/json", - "Authorization" to "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24yIn0.RA8ERppuFmastqFN-6C98WqMEE7L6h88WylMeq6jh1w", + "Authorization" to + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJwZXJtaXNzaW9ucyI6InBlcm1pc3Npb24yIn0.RA8ERppuFmastqFN-6C98WqMEE7L6h88WylMeq6jh1w", ), ), mockk(), @@ -612,8 +602,7 @@ class RequestHandlerTest { mapOf( "testQueryParam" to "foo", ), - ) - .withMultiValueQueryStringParameters( + ).withMultiValueQueryStringParameters( mapOf( "testMultiValueQueryStringParam" to listOf("foo", "bar"), ), @@ -801,9 +790,15 @@ class RequestHandlerTest { } class TestRequestHandler : RequestHandler() { - data class TestResponse(val greeting: String) + data class TestResponse( + val greeting: String, + ) - data class TestRequest(val greeting: String, val age: Int = 0, val bday: LocalDate = LocalDate.now()) + data class TestRequest( + val greeting: String, + val age: Int = 0, + val bday: LocalDate = LocalDate.now(), + ) override val router = router { @@ -855,11 +850,12 @@ class RequestHandlerTest { POST("/somes") { r: Request> -> ResponseEntity.ok( - r.body.map { - TestResponse( - it.greeting, - ) - }.toList(), + r.body + .map { + TestResponse( + it.greeting, + ) + }.toList(), ) } POST("/no-content") { _: Request -> @@ -897,7 +893,10 @@ class RequestHandlerTest { } class AcceptTypeDependingHandler : RequestHandler() { - data class CustomObject(val text: String, val number: Int) + data class CustomObject( + val text: String, + val number: Int, + ) override val router = router { diff --git a/router/src/test/kotlin/io/moia/router/RouterTest.kt b/router/src/test/kotlin/io/moia/router/RouterTest.kt index f0e2a44..b312291 100644 --- a/router/src/test/kotlin/io/moia/router/RouterTest.kt +++ b/router/src/test/kotlin/io/moia/router/RouterTest.kt @@ -64,8 +64,7 @@ class RouterTest { router { POST("/some") { r: Request -> ResponseEntity.ok("""{"hello": "world", "request":"${r.body}"}""") - } - .producing("text/plain") + }.producing("text/plain") .consuming("text/plain") }