From 95f73b930258b919111fd28d2bcb332b073a0913 Mon Sep 17 00:00:00 2001 From: Dmitriy Govorukhin Date: Wed, 28 Jun 2023 07:02:18 +0200 Subject: [PATCH 1/3] m3l3 - ok-marketplace-app-serverless --- gradle.properties | 3 + ok-marketplace-app-serverless/.gitignore | 2 + .../build.gradle.kts | 31 +++++ .../serverlessapp/api/IHandleStrategy.kt | 12 ++ .../serverlessapp/api/RoutingHandler.kt | 42 +++++++ .../serverlessapp/api/model/Request.kt | 35 ++++++ .../serverlessapp/api/model/Response.kt | 8 ++ .../serverlessapp/api/utils/conversions.kt | 64 ++++++++++ .../serverlessapp/api/v1/IV1HandleStrategy.kt | 22 ++++ .../serverlessapp/api/v1/handlers.kt | 90 ++++++++++++++ .../serverlessapp/api/v1/routing.kt | 11 ++ .../serverlessapp/api/v2/IV1HandleStrategy.kt | 22 ++++ .../serverlessapp/api/v2/handlers.kt | 89 ++++++++++++++ .../serverlessapp/api/v2/routing.kt | 12 ++ ok-marketplace-app-serverless/src/tf/main.tf | 113 ++++++++++++++++++ readme.md | 1 + settings.gradle.kts | 3 + 17 files changed, 560 insertions(+) create mode 100644 ok-marketplace-app-serverless/.gitignore create mode 100644 ok-marketplace-app-serverless/build.gradle.kts create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/IHandleStrategy.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/RoutingHandler.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/model/Request.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/model/Response.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/utils/conversions.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/IV1HandleStrategy.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/handlers.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/routing.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/IV1HandleStrategy.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/handlers.kt create mode 100644 ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/routing.kt create mode 100644 ok-marketplace-app-serverless/src/tf/main.tf diff --git a/gradle.properties b/gradle.properties index b60d3e2..c2f4450 100644 --- a/gradle.properties +++ b/gradle.properties @@ -29,3 +29,6 @@ springDependencyManagementVersion=1.1.0 pluginSpringVersion=1.8.10 pluginJpa=1.8.10 springdocOpenapiUiVersion=2.2.0 + +pluginShadow=7.1.2 +yandexCloudSdkVersion=2.5.1 \ No newline at end of file diff --git a/ok-marketplace-app-serverless/.gitignore b/ok-marketplace-app-serverless/.gitignore new file mode 100644 index 0000000..9c553ab --- /dev/null +++ b/ok-marketplace-app-serverless/.gitignore @@ -0,0 +1,2 @@ +**/tf/* +!**/tf/main.tf \ No newline at end of file diff --git a/ok-marketplace-app-serverless/build.gradle.kts b/ok-marketplace-app-serverless/build.gradle.kts new file mode 100644 index 0000000..3f433b9 --- /dev/null +++ b/ok-marketplace-app-serverless/build.gradle.kts @@ -0,0 +1,31 @@ +val jacksonVersion: String by project +val serializationVersion: String by project +val yandexCloudSdkVersion: String by project + +plugins { + kotlin("jvm") + id("com.github.johnrengelman.shadow") +} + +dependencies { + implementation("com.yandex.cloud:java-sdk-functions:$yandexCloudSdkVersion") + implementation(kotlin("stdlib-jdk8")) + + implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") + implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") + implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion") + + // transport models + implementation(project(":ok-marketplace-common")) + implementation(project(":ok-marketplace-api-v1-jackson")) + implementation(project(":ok-marketplace-api-v2-kmp")) + implementation(project(":ok-marketplace-mappers-v1")) + implementation(project(":ok-marketplace-mappers-v2")) + + // Stubs + implementation(project(":ok-marketplace-stubs")) +} \ No newline at end of file diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/IHandleStrategy.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/IHandleStrategy.kt new file mode 100644 index 0000000..6f9bb69 --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/IHandleStrategy.kt @@ -0,0 +1,12 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api + +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Request +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Response +import ru.otus.otuskotlin.marketplace.serverlessapp.api.v1.* +import yandex.cloud.sdk.functions.Context + +interface IHandleStrategy { + val version: String + val path: String + fun handle(req: Request, reqContext: Context): Response +} \ No newline at end of file diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/RoutingHandler.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/RoutingHandler.kt new file mode 100644 index 0000000..f512ef7 --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/RoutingHandler.kt @@ -0,0 +1,42 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api + +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Request +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Response +import ru.otus.otuskotlin.marketplace.serverlessapp.api.v1.IV1HandleStrategy.Companion.V1 +import ru.otus.otuskotlin.marketplace.serverlessapp.api.v1.v1handlers +import ru.otus.otuskotlin.marketplace.serverlessapp.api.v2.IV2HandleStrategy.Companion.V2 +import ru.otus.otuskotlin.marketplace.serverlessapp.api.v2.v2handlers +import yandex.cloud.sdk.functions.Context +import yandex.cloud.sdk.functions.YcFunction + +@Suppress("unused") +class RoutingHandler : YcFunction { + + override fun handle(event: Request, context: Context): Response = + try { + println(event) + val validationResponse = validate(event) + val url = event.url!! + when { + validationResponse != null -> validationResponse + url.isVersion(V1) -> v1handlers(event, context) + url.isVersion(V2) -> v2handlers(event, context) + else -> Response(400, false, emptyMap(), "Unknown api version! Path: ${event.url}") + } + } catch (e: Exception) { + Response(500, false, emptyMap(), "Unknown error: ${e.message}") + } + + /** + * Validate input event. + */ + private fun validate(event: Request): Response? = + when { + event.httpMethod != "POST" -> Response(400, false, emptyMap(), "Invalid http method: ${event.httpMethod}") + event.url == null -> Response(400, false, emptyMap(), "Invalid url") + event.body == null -> Response(400, false, emptyMap(), "Invalid body") + else -> null + } + + private fun String.isVersion(versionPrefix: String): Boolean = startsWith("/$versionPrefix") +} diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/model/Request.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/model/Request.kt new file mode 100644 index 0000000..3c1d07b --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/model/Request.kt @@ -0,0 +1,35 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api.model + +data class Request( + var httpMethod: String? = null, + var headers: Map? = null, + var url: String? = null, + var body: String? = null, + var path: String? = null, + var params: Map? = null, + var multiValueParams: Map>? = null, + var pathParams: Map? = null, + var version: String? = null, + var resource: String? = null, + var multiValueHeaders: Map>? = null, + var queryStringParameters: Map? = null, + var requestContext: RequestContext? = null, + var pathParameters: Map? = null, + var isBase64Encoded: Boolean = false, + var parameters: Map? = null, + var multiValueParameters: Map>? = null, + var operationId: String? = null, +) + +data class RequestContext( + var identity: Identity? = null, + var httpMethod: String? = null, + var requestId: String? = null, + var requestTime: String? = null, + var requestTimeEpoch: Long? = null, +) + +data class Identity( + var sourceIp: String? = null, + var userAgent: String? = null, +) \ No newline at end of file diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/model/Response.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/model/Response.kt new file mode 100644 index 0000000..7076dc3 --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/model/Response.kt @@ -0,0 +1,8 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api.model + +data class Response( + val statusCode: Int, + val isBase64Encoded: Boolean, + val headers: Map, + val body: String +) \ No newline at end of file diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/utils/conversions.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/utils/conversions.kt new file mode 100644 index 0000000..51dc491 --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/utils/conversions.kt @@ -0,0 +1,64 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api.utils + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import kotlinx.datetime.Clock +import kotlinx.serialization.encodeToString +import ru.otus.otuskotlin.marketplace.api.v2.apiV2Mapper +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.MkplRequestId +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Request +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Response +import yandex.cloud.sdk.functions.Context +import java.util.* + +/** + * Input: /v1/ad/create? + * Output: ad/create + */ +fun String.dropVersionPrefix(versionPrefix: String) = + "^\\/$versionPrefix\\/([^?]*)\\??\$".toRegex() + .findAll(this) + .firstOrNull() + ?.groupValues + ?.get(1) + +val objectMapper: ObjectMapper = jacksonObjectMapper().findAndRegisterModules() + +inline fun Request.toTransportModel(): T = + if (isBase64Encoded) { + objectMapper.readValue(Base64.getDecoder().decode(body)) + } else { + objectMapper.readValue(body!!) + } + +fun withContext(context: Context, block: MkplContext.() -> Response) = + with( + MkplContext( + timeStart = Clock.System.now(), + requestId = MkplRequestId(context.requestId) + ) + ) { + block() + } + +/** + * V1 + */ +fun ru.otus.otuskotlin.marketplace.api.v1.models.IResponse.toResponse(): Response = + toResponse(objectMapper.writeValueAsString(this)) + +/** + * V2 + */ +fun ru.otus.otuskotlin.marketplace.api.v2.models.IResponse.toResponse(): Response = + toResponse(apiV2Mapper.encodeToString(this)) + +private fun toResponse(body: String): Response = + Response( + 200, + false, + mapOf("Content-Type" to "application/json"), + body, + ) \ No newline at end of file diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/IV1HandleStrategy.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/IV1HandleStrategy.kt new file mode 100644 index 0000000..e69bb04 --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/IV1HandleStrategy.kt @@ -0,0 +1,22 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api.v1 + +import ru.otus.otuskotlin.marketplace.serverlessapp.api.IHandleStrategy + +sealed interface IV1HandleStrategy : IHandleStrategy { + override val version: String + get() = V1 + + companion object { + const val V1 = "v1" + private val strategies = listOf( + CreateAdHandler, + ReadAdHandler, + UpdateAdHandler, + DeleteAdHandler, + SearchAdHandler, + OffersAdHandler + ) + val strategiesByDiscriminator = strategies.associateBy { it.path } + } +} + diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/handlers.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/handlers.kt new file mode 100644 index 0000000..d9f94b3 --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/handlers.kt @@ -0,0 +1,90 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api.v1 + +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Request +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Response +import ru.otus.otuskotlin.marketplace.serverlessapp.api.utils.toResponse +import ru.otus.otuskotlin.marketplace.serverlessapp.api.utils.toTransportModel +import ru.otus.otuskotlin.marketplace.api.v1.models.* +import ru.otus.otuskotlin.marketplace.mappers.v1.* +import ru.otus.otuskotlin.marketplace.serverlessapp.api.utils.withContext +import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub +import yandex.cloud.sdk.functions.Context + +object CreateAdHandler : IV1HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("CreateAdHandler v1 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adResponse = MkplAdStub.get() + toTransportCreate().toResponse() + } + } +} + +object ReadAdHandler : IV1HandleStrategy { + override val path: String = "ad/read" + override fun handle(req: Request, reqContext: Context): Response { + println("ReadAdHandler v1 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adResponse = MkplAdStub.get() + toTransportRead().toResponse() + } + } +} + +object UpdateAdHandler : IV1HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + + println("UpdateAdHandler v1 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adResponse = MkplAdStub.get() + toTransportUpdate().toResponse() + } + + } +} + +object DeleteAdHandler : IV1HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("DeleteAdHandler v1 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + toTransportDelete().toResponse() + } + } +} + +object SearchAdHandler : IV1HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("SearchAdHandler v1 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adResponse = MkplAdStub.get() + toTransportSearch().toResponse() + } + } +} + +object OffersAdHandler : IV1HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("OffersAdHandler v1 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adsResponse.add(MkplAdStub.get()) + toTransportOffers().toResponse() + } + } +} \ No newline at end of file diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/routing.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/routing.kt new file mode 100644 index 0000000..be71d37 --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v1/routing.kt @@ -0,0 +1,11 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api.v1 + +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Request +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Response +import ru.otus.otuskotlin.marketplace.serverlessapp.api.utils.dropVersionPrefix +import yandex.cloud.sdk.functions.Context + +fun v1handlers(req: Request, reqContext: Context): Response = + IV1HandleStrategy.strategiesByDiscriminator[req.url!!.dropVersionPrefix(IV1HandleStrategy.V1)] + ?.handle(req, reqContext) + ?: Response(400, false, emptyMap(), "Unknown path! Path: ${req.url}") \ No newline at end of file diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/IV1HandleStrategy.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/IV1HandleStrategy.kt new file mode 100644 index 0000000..5759606 --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/IV1HandleStrategy.kt @@ -0,0 +1,22 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api.v2 + +import ru.otus.otuskotlin.marketplace.serverlessapp.api.IHandleStrategy + +sealed interface IV2HandleStrategy : IHandleStrategy { + override val version: String + get() = V2 + + companion object { + const val V2 = "v2" + private val strategies = listOf( + CreateAdHandler, + ReadAdHandler, + UpdateAdHandler, + DeleteAdHandler, + SearchAdHandler, + OffersAdHandler + ) + val strategiesByDiscriminator = strategies.associateBy { it.path } + } +} + diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/handlers.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/handlers.kt new file mode 100644 index 0000000..fdf597c --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/handlers.kt @@ -0,0 +1,89 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api.v2 + +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Request +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Response +import ru.otus.otuskotlin.marketplace.serverlessapp.api.utils.toResponse +import ru.otus.otuskotlin.marketplace.serverlessapp.api.utils.toTransportModel +import ru.otus.otuskotlin.marketplace.api.v2.models.* +import ru.otus.otuskotlin.marketplace.mappers.v2.fromTransport +import ru.otus.otuskotlin.marketplace.mappers.v2.toTransportCreate +import ru.otus.otuskotlin.marketplace.serverlessapp.api.utils.withContext +import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub +import yandex.cloud.sdk.functions.Context + +object CreateAdHandler : IV2HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("CreateAdHandler v2 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adResponse = MkplAdStub.get() + toTransportCreate().toResponse() + } + } +} + +object ReadAdHandler : IV2HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("ReadAdHandler v2 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adResponse = MkplAdStub.get() + toTransportCreate().toResponse() + } + } +} + +object UpdateAdHandler : IV2HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("UpdateAdHandler v2 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adResponse = MkplAdStub.get() + toTransportCreate().toResponse() + } + } +} + +object DeleteAdHandler : IV2HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("DeleteAdHandler v2 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + toTransportCreate().toResponse() + } + } +} + +object SearchAdHandler : IV2HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("SearchAdHandler v2 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adResponse = MkplAdStub.get() + toTransportCreate().toResponse() + } + } +} + +object OffersAdHandler : IV2HandleStrategy { + override val path: String = "ad/create" + override fun handle(req: Request, reqContext: Context): Response { + println("OffersAdHandler v2 start work") + val request = req.toTransportModel() + return withContext(reqContext) { + fromTransport(request) + adsResponse.add(MkplAdStub.get()) + toTransportCreate().toResponse() + } + } +} \ No newline at end of file diff --git a/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/routing.kt b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/routing.kt new file mode 100644 index 0000000..ed97a54 --- /dev/null +++ b/ok-marketplace-app-serverless/src/main/kotlin/ru/otus/otuskotlin/marketplace/serverlessapp/api/v2/routing.kt @@ -0,0 +1,12 @@ +package ru.otus.otuskotlin.marketplace.serverlessapp.api.v2 + + +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Request +import ru.otus.otuskotlin.marketplace.serverlessapp.api.model.Response +import ru.otus.otuskotlin.marketplace.serverlessapp.api.utils.dropVersionPrefix +import yandex.cloud.sdk.functions.Context + +fun v2handlers(req: Request, reqContext: Context): Response = + IV2HandleStrategy.strategiesByDiscriminator[req.url!!.dropVersionPrefix(IV2HandleStrategy.V2)] + ?.handle(req, reqContext) + ?: Response(400, false, emptyMap(), "Unknown path! Path: ${req.url}") \ No newline at end of file diff --git a/ok-marketplace-app-serverless/src/tf/main.tf b/ok-marketplace-app-serverless/src/tf/main.tf new file mode 100644 index 0000000..457c34d --- /dev/null +++ b/ok-marketplace-app-serverless/src/tf/main.tf @@ -0,0 +1,113 @@ +terraform { + required_providers { + yandex = { + source = "yandex-cloud/yandex" + } + } + required_version = ">= 0.13" +} + +locals { + bucket = "ok-marketplace-terraform" + version = "1" + jar_name = "marketplace-serverless-${local.version}.jar" + + // Secrets + YC_TOKEN = "" + YC_CLOUD_ID = "" + YC_FOLDER_ID = "" +} + + +provider "yandex" { + token = local.YC_TOKEN + folder_id = local.YC_FOLDER_ID + zone = "ru-central1-a" +} + +resource "yandex_iam_service_account" "sa" { + name = "terraform-sa" +} + +# Создание статического ключа доступа +resource "yandex_iam_service_account_static_access_key" "sa-static-key" { + service_account_id = yandex_iam_service_account.sa.id + description = "static access key for object storage" +} + +# Назначаем роль сервисному аккаунту +resource "yandex_resourcemanager_folder_iam_member" "sa-editor" { + folder_id = local.YC_FOLDER_ID + role = "storage.editor" + member = "serviceAccount:${yandex_iam_service_account.sa.id}" +} + +# Создание бакета с использованием ключа +resource "yandex_storage_bucket" "jar-bucket" { + access_key = yandex_iam_service_account_static_access_key.sa-static-key.access_key + secret_key = yandex_iam_service_account_static_access_key.sa-static-key.secret_key + bucket = local.bucket +} + +# Загрузка jar в бакет +resource "yandex_storage_object" "marketplace-serverless-jar" { + access_key = yandex_iam_service_account_static_access_key.sa-static-key.access_key + secret_key = yandex_iam_service_account_static_access_key.sa-static-key.secret_key + bucket = local.bucket + key = local.jar_name + source = "../../build/libs/ok-marketplace-app-serverless-0.0.1-SNAPSHOT-all.jar" +} + + +# Создаем gateway для проксирования запросов в функцию +resource "yandex_api_gateway" "test-api-gateway" { + name = "ok-marketplace-gateway" + description = "Serverless marketplace gateway" + spec = <<-EOT + openapi: "3.0.0" + info: + version: 1.0.0 + title: Test API + paths: + /{proxy+}: + post: + summary: all + operationId: anyId + parameters: + - name: proxy + in: path + description: path + required: true + schema: + type: string + x-yc-apigateway-integration: + type: cloud_functions + function_id: ${yandex_function.marketplace-serverless.id} + tag: "$latest" + service_account_id: ${yandex_iam_service_account.sa.id} + EOT +} + +# Создание функции +resource "yandex_function" "marketplace-serverless" { + name = "marketplace-serverless" + description = "marketplace serverless" + user_hash = "marketplace-serverless-${local.version}" + runtime = "java11" + entrypoint = "ru.otus.otuskotlin.marketplace.serverlessapp.api.RoutingHandler" + memory = "128" + execution_timeout = "10" + package { + bucket_name = local.bucket + object_name = local.jar_name + } +} + +# Разрешаем вызывать функцию сервисному аккаунту +resource "yandex_function_iam_binding" "function-iam" { + function_id = yandex_function.marketplace-serverless.id + role = "serverless.functions.invoker" + members = [ + "serviceAccount:${yandex_iam_service_account.sa.id}", + ] +} diff --git a/readme.md b/readme.md index c77d1b0..d5d46f4 100644 --- a/readme.md +++ b/readme.md @@ -48,6 +48,7 @@ Marketplace -- это площадка, на которой пользовате 1. [ok-marketplace-app-common](ok-marketplace-app-common) Общий код для приложений 2. [ok-marketplace-app-spring](ok-marketplace-app-spring) Spring 3. [ok-marketplace-app-ktor](ok-marketplace-app-ktor) Ktor + 4. [ok-marketplace-app-serverless](ok-marketplace-app-serverless) Яндекс-облако ## Подпроекты для занятий по языку Kotlin diff --git a/settings.gradle.kts b/settings.gradle.kts index 5e8d0cd..3bd96a4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,7 @@ pluginManagement { val springDependencyManagementVersion: String by settings val pluginSpringVersion: String by settings val pluginJpa: String by settings + val pluginShadow: String by settings val ktorVersion: String by settings plugins { @@ -22,6 +23,7 @@ pluginManagement { id("io.ktor.plugin") version ktorVersion apply false id("org.openapi.generator") version openapiVersion apply false + id("com.github.johnrengelman.shadow") version pluginShadow apply false } } @@ -49,3 +51,4 @@ include("ok-marketplace-biz") include("ok-marketplace-app-spring") include("ok-marketplace-app-ktor") +include("ok-marketplace-app-serverless") From d38e34073462f635a90cba2859bef645d85d737a Mon Sep 17 00:00:00 2001 From: BarracudaPff Date: Fri, 12 May 2023 10:27:02 +0200 Subject: [PATCH 2/3] [m3l4-websocket] add websocket api to spring --- ok-marketplace-app-spring/build.gradle.kts | 2 +- .../springapp/api/v1/ws/WebSocketConfigV1.kt | 15 ++++ .../springapp/api/v1/ws/WsAdHandlerV1.kt | 63 ++++++++++++++ .../springapp/api/v2/ws/WebSocketConfigV2.kt | 15 ++++ .../springapp/api/v2/ws/WsAdHandlerV2.kt | 69 +++++++++++++++ .../api/v1/controller/WsControllerTest.kt | 84 +++++++++++++++++++ 6 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/ws/WebSocketConfigV1.kt create mode 100644 ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/ws/WsAdHandlerV1.kt create mode 100644 ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v2/ws/WebSocketConfigV2.kt create mode 100644 ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v2/ws/WsAdHandlerV2.kt create mode 100644 ok-marketplace-app-spring/src/test/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/controller/WsControllerTest.kt diff --git a/ok-marketplace-app-spring/build.gradle.kts b/ok-marketplace-app-spring/build.gradle.kts index 4e67ddb..510e524 100644 --- a/ok-marketplace-app-spring/build.gradle.kts +++ b/ok-marketplace-app-spring/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") // info; refresh; springMvc output implementation("org.springframework.boot:spring-boot-starter-webflux") // Controller, Service, etc.. - // implementation("org.springframework.boot:spring-boot-starter-websocket") // Controller, Service, etc.. + implementation("org.springframework.boot:spring-boot-starter-websocket") // Controller, Service, etc.. implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:$springdocOpenapiUiVersion") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") // from models to json and Vice versa implementation("org.jetbrains.kotlin:kotlin-reflect") // for spring-boot app diff --git a/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/ws/WebSocketConfigV1.kt b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/ws/WebSocketConfigV1.kt new file mode 100644 index 0000000..a43ddaa --- /dev/null +++ b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/ws/WebSocketConfigV1.kt @@ -0,0 +1,15 @@ +package ru.otus.otuskotlin.markeplace.springapp.api.v1.ws + +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.config.annotation.EnableWebSocket +import org.springframework.web.socket.config.annotation.WebSocketConfigurer +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry + +@Configuration +@EnableWebSocket +class WebSocketConfigV1(val handlerV1: WsAdHandlerV1) : WebSocketConfigurer { + + override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) { + registry.addHandler(handlerV1, "/ws/v1").setAllowedOrigins("*") + } +} diff --git a/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/ws/WsAdHandlerV1.kt b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/ws/WsAdHandlerV1.kt new file mode 100644 index 0000000..ffd7dee --- /dev/null +++ b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/ws/WsAdHandlerV1.kt @@ -0,0 +1,63 @@ +package ru.otus.otuskotlin.markeplace.springapp.api.v1.ws + +import kotlinx.datetime.Clock +import org.springframework.stereotype.Component +import org.springframework.web.socket.CloseStatus +import org.springframework.web.socket.TextMessage +import org.springframework.web.socket.WebSocketSession +import org.springframework.web.socket.handler.TextWebSocketHandler +import ru.otus.otuskotlin.markeplace.springapp.service.MkplAdBlockingProcessor +import ru.otus.otuskotlin.marketplace.api.v1.apiV1Mapper +import ru.otus.otuskotlin.marketplace.api.v1.models.IRequest +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.asMkplError +import ru.otus.otuskotlin.marketplace.common.helpers.isUpdatableCommand +import ru.otus.otuskotlin.marketplace.common.models.MkplWorkMode +import ru.otus.otuskotlin.marketplace.mappers.v1.fromTransport +import ru.otus.otuskotlin.marketplace.mappers.v1.toTransportAd +import ru.otus.otuskotlin.marketplace.mappers.v1.toTransportInit +import java.util.concurrent.ConcurrentHashMap + +@Component +class WsAdHandlerV1(private val processor: MkplAdBlockingProcessor) : TextWebSocketHandler() { + private val sessions = ConcurrentHashMap() + + override fun afterConnectionEstablished(session: WebSocketSession) { + sessions[session.id] = session + + val context = MkplContext() + // TODO убрать, когда заработает обычный режим + context.workMode = MkplWorkMode.STUB + processor.exec(context) + + val msg = apiV1Mapper.writeValueAsString(context.toTransportInit()) + session.sendMessage(TextMessage(msg)) + } + + override fun handleTextMessage(session: WebSocketSession, message: TextMessage) { + val ctx = MkplContext(timeStart = Clock.System.now()) + try { + val request = apiV1Mapper.readValue(message.payload, IRequest::class.java) + ctx.fromTransport(request) + processor.exec(ctx) + + val result = apiV1Mapper.writeValueAsString(ctx.toTransportAd()) + if (ctx.isUpdatableCommand()) { + sessions.values.forEach { + it.sendMessage(TextMessage(result)) + } + } else { + session.sendMessage(TextMessage(result)) + } + } catch (e: Exception) { + ctx.errors.add(e.asMkplError()) + + val response = apiV1Mapper.writeValueAsString(ctx.toTransportInit()) + session.sendMessage(TextMessage(response)) + } + } + + override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) { + sessions.remove(session.id) + } +} diff --git a/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v2/ws/WebSocketConfigV2.kt b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v2/ws/WebSocketConfigV2.kt new file mode 100644 index 0000000..b28c0cc --- /dev/null +++ b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v2/ws/WebSocketConfigV2.kt @@ -0,0 +1,15 @@ +package ru.otus.otuskotlin.markeplace.springapp.api.v2.ws + +import org.springframework.context.annotation.Configuration +import org.springframework.web.socket.config.annotation.EnableWebSocket +import org.springframework.web.socket.config.annotation.WebSocketConfigurer +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry + +@Configuration +@EnableWebSocket +class WebSocketConfigV2(val handlerV2: WsAdHandlerV2) : WebSocketConfigurer { + + override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) { + registry.addHandler(handlerV2, "/ws/v2").setAllowedOrigins("*") + } +} diff --git a/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v2/ws/WsAdHandlerV2.kt b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v2/ws/WsAdHandlerV2.kt new file mode 100644 index 0000000..8f403e5 --- /dev/null +++ b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v2/ws/WsAdHandlerV2.kt @@ -0,0 +1,69 @@ +package ru.otus.otuskotlin.markeplace.springapp.api.v2.ws + +import kotlinx.datetime.Clock +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import org.springframework.stereotype.Component +import org.springframework.web.socket.CloseStatus +import org.springframework.web.socket.TextMessage +import org.springframework.web.socket.WebSocketSession +import org.springframework.web.socket.handler.TextWebSocketHandler +import ru.otus.otuskotlin.markeplace.springapp.service.MkplAdBlockingProcessor +import ru.otus.otuskotlin.marketplace.api.v2.apiV2Mapper +import ru.otus.otuskotlin.marketplace.api.v2.apiV2ResponseSerialize +import ru.otus.otuskotlin.marketplace.api.v2.models.IRequest +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.asMkplError +import ru.otus.otuskotlin.marketplace.common.helpers.isUpdatableCommand +import ru.otus.otuskotlin.marketplace.common.models.MkplWorkMode +import ru.otus.otuskotlin.marketplace.mappers.v2.fromTransport +import ru.otus.otuskotlin.marketplace.mappers.v2.toTransportAd +import ru.otus.otuskotlin.marketplace.mappers.v2.toTransportInit +import java.util.concurrent.ConcurrentHashMap + +@Component +class +WsAdHandlerV2(private val processor: MkplAdBlockingProcessor) : TextWebSocketHandler() { + private val sessions = ConcurrentHashMap() + + override fun afterConnectionEstablished(session: WebSocketSession) { + sessions[session.id] = session + + val context = MkplContext() + // TODO убрать, когда заработает обычный режим + context.workMode = MkplWorkMode.STUB + processor.exec(context) + + val msg = apiV2ResponseSerialize(context.toTransportInit()) + session.sendMessage(TextMessage(msg)) + } + + override fun handleTextMessage(session: WebSocketSession, message: TextMessage) { + val ctx = MkplContext(timeStart = Clock.System.now()) + + try { + val request = apiV2Mapper.decodeFromString(message.payload) + ctx.fromTransport(request) + processor.exec(ctx) + + val result = apiV2Mapper.encodeToString(ctx.toTransportAd()) + + if (ctx.isUpdatableCommand()) { + sessions.values.forEach { + it.sendMessage(TextMessage(result)) + } + } else { + session.sendMessage(TextMessage(result)) + } + } catch (e: Exception) { + ctx.errors.add(e.asMkplError()) + + val response = apiV2ResponseSerialize(ctx.toTransportInit()) + session.sendMessage(TextMessage(response)) + } + } + + override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) { + sessions.remove(session.id) + } +} diff --git a/ok-marketplace-app-spring/src/test/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/controller/WsControllerTest.kt b/ok-marketplace-app-spring/src/test/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/controller/WsControllerTest.kt new file mode 100644 index 0000000..ea037aa --- /dev/null +++ b/ok-marketplace-app-spring/src/test/kotlin/ru/otus/otuskotlin/markeplace/springapp/api/v1/controller/WsControllerTest.kt @@ -0,0 +1,84 @@ +package ru.otus.otuskotlin.markeplace.springapp.api.v1.controller + +import io.kotest.common.runBlocking +import jakarta.websocket.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import ru.otus.otuskotlin.marketplace.api.v1.apiV1Mapper +import ru.otus.otuskotlin.marketplace.api.v1.models.AdInitResponse +import ru.otus.otuskotlin.marketplace.api.v1.models.IResponse +import ru.otus.otuskotlin.marketplace.api.v1.models.ResponseResult +import java.net.URI + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class WsControllerTest { + + @LocalServerPort + private var port: Int = 0 + + private lateinit var container: WebSocketContainer + private lateinit var client: TestWebSocketClient + + @BeforeEach + fun setup() { + container = ContainerProvider.getWebSocketContainer() + client = TestWebSocketClient() + } + + @Test + fun initSession() { + runBlocking { + withContext(Dispatchers.IO) { + container.connectToServer(client, URI.create("ws://localhost:${port}/ws/v1")) + } + + withTimeout(3000) { + while (client.session?.isOpen != true) { + delay(100) + } + } + assert(client.session?.isOpen == true) + withTimeout(3000) { + val incame = client.receive() + val message = apiV1Mapper.readValue(incame, IResponse::class.java) + assert(message is AdInitResponse) + assert(message.result == ResponseResult.SUCCESS) + } + } + } +} + +@ClientEndpoint +class TestWebSocketClient { + var session: Session? = null + private val messages: MutableList = mutableListOf() + + @OnOpen + fun onOpen(session: Session?) { + this.session = session + } + + @OnClose + fun onClose(session: Session?, reason: CloseReason) { + this.session = null + } + + @OnMessage + fun onMessage(message: String) { + messages.add(message) + } + + suspend fun receive(): String { + while (messages.isEmpty()) { + delay(100) + } + return messages.removeFirst() + } +} From 5ba6c40186db072e75a98205b9196464d67f7699 Mon Sep 17 00:00:00 2001 From: BarracudaPff Date: Fri, 12 May 2023 10:27:14 +0200 Subject: [PATCH 3/3] [m3l4-websocket] add websocket api to ktor --- ok-marketplace-app-ktor/build.gradle.kts | 4 + .../src/commonMain/kotlin/Application.kt | 16 +- .../src/commonMain/kotlin/v2/WsController.kt | 68 ++++++++ .../src/jvmMain/kotlin/ApplicationJvm.kt | 19 ++- .../src/jvmMain/kotlin/v1/WsController.kt | 64 ++++++++ .../kotlin/stubs/V1WebsocketStubTest.kt | 149 +++++++++++++++++ .../kotlin/stubs/V2WebsocketStubTest.kt | 152 ++++++++++++++++++ .../service/MkplAdBlockingProcessor.kt | 13 ++ 8 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 ok-marketplace-app-ktor/src/commonMain/kotlin/v2/WsController.kt create mode 100644 ok-marketplace-app-ktor/src/jvmMain/kotlin/v1/WsController.kt create mode 100644 ok-marketplace-app-ktor/src/jvmTest/kotlin/stubs/V1WebsocketStubTest.kt create mode 100644 ok-marketplace-app-ktor/src/jvmTest/kotlin/stubs/V2WebsocketStubTest.kt create mode 100644 ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/service/MkplAdBlockingProcessor.kt diff --git a/ok-marketplace-app-ktor/build.gradle.kts b/ok-marketplace-app-ktor/build.gradle.kts index cbf7789..b49366f 100644 --- a/ok-marketplace-app-ktor/build.gradle.kts +++ b/ok-marketplace-app-ktor/build.gradle.kts @@ -16,6 +16,10 @@ plugins { kotlin("multiplatform") id("io.ktor.plugin") } +dependencies { + implementation("io.ktor:ktor-server-core-jvm:2.2.4") + implementation("io.ktor:ktor-server-websockets-jvm:2.2.4") +} repositories { maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") } diff --git a/ok-marketplace-app-ktor/src/commonMain/kotlin/Application.kt b/ok-marketplace-app-ktor/src/commonMain/kotlin/Application.kt index 3cd8afd..1c0a2c3 100644 --- a/ok-marketplace-app-ktor/src/commonMain/kotlin/Application.kt +++ b/ok-marketplace-app-ktor/src/commonMain/kotlin/Application.kt @@ -7,12 +7,18 @@ import io.ktor.server.engine.* import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.ktor.server.websocket.* import ru.otus.otuskotlin.marketplace.api.v2.apiV2Mapper import ru.otus.otuskotlin.marketplace.app.v2.v2Ad import ru.otus.otuskotlin.marketplace.app.v2.v2Offer +import ru.otus.otuskotlin.marketplace.app.v2.wsHandlerV2 import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor -fun Application.module(processor: MkplAdProcessor = MkplAdProcessor()) { +fun Application.module(processor: MkplAdProcessor = MkplAdProcessor(), installPlugins: Boolean = true) { + if (installPlugins) { + install(WebSockets) + } + routing { get("/") { call.respondText("Hello, world!") @@ -25,11 +31,13 @@ fun Application.module(processor: MkplAdProcessor = MkplAdProcessor()) { v2Ad(processor) v2Offer(processor) } + + webSocket("/ws/v2") { + wsHandlerV2(processor) + } } } fun main() { - embeddedServer(CIO, port = 8080) { - module() - }.start(wait = true) + embeddedServer(CIO, port = 8080, module = Application::module).start(wait = true) } diff --git a/ok-marketplace-app-ktor/src/commonMain/kotlin/v2/WsController.kt b/ok-marketplace-app-ktor/src/commonMain/kotlin/v2/WsController.kt new file mode 100644 index 0000000..164d8f4 --- /dev/null +++ b/ok-marketplace-app-ktor/src/commonMain/kotlin/v2/WsController.kt @@ -0,0 +1,68 @@ +package ru.otus.otuskotlin.marketplace.app.v2 + +import io.ktor.websocket.* +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.serialization.decodeFromString +import ru.otus.otuskotlin.marketplace.api.v2.apiV2Mapper +import ru.otus.otuskotlin.marketplace.api.v2.apiV2ResponseSerialize +import ru.otus.otuskotlin.marketplace.api.v2.models.IRequest +import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.addError +import ru.otus.otuskotlin.marketplace.common.helpers.asMkplError +import ru.otus.otuskotlin.marketplace.common.helpers.isUpdatableCommand +import ru.otus.otuskotlin.marketplace.common.models.MkplWorkMode +import ru.otus.otuskotlin.marketplace.mappers.v2.fromTransport +import ru.otus.otuskotlin.marketplace.mappers.v2.toTransportAd +import ru.otus.otuskotlin.marketplace.mappers.v2.toTransportInit +import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub + +val sessions = mutableSetOf() + +suspend fun WebSocketSession.wsHandlerV2(processor: MkplAdProcessor) { + sessions.add(this) + + // Handle init request + val ctx = MkplContext() + ctx.workMode = MkplWorkMode.STUB + processor.exec(ctx) + + val init = apiV2ResponseSerialize(ctx.toTransportInit()) + outgoing.send(Frame.Text(init)) + + // Handle flow + incoming.receiveAsFlow().mapNotNull { it -> + val frame = it as? Frame.Text ?: return@mapNotNull + + val jsonStr = frame.readText() + val context = MkplContext() + + // Handle without flow destruction + try { + val request = apiV2Mapper.decodeFromString(jsonStr) + context.fromTransport(request) + processor.exec(context) + + val result = apiV2ResponseSerialize(context.toTransportAd()) + + // If change request, response is sent to everyone + if (context.isUpdatableCommand()) { + sessions.forEach { + it.send(Frame.Text(result)) + } + } else { + outgoing.send(Frame.Text(result)) + } + } catch (_: ClosedReceiveChannelException) { + sessions.clear() + } catch (t: Throwable) { + context.addError(t.asMkplError()) + + val result = apiV2ResponseSerialize(context.toTransportInit()) + outgoing.send(Frame.Text(result)) + } + }.collect() +} diff --git a/ok-marketplace-app-ktor/src/jvmMain/kotlin/ApplicationJvm.kt b/ok-marketplace-app-ktor/src/jvmMain/kotlin/ApplicationJvm.kt index d359e3f..acdbb25 100644 --- a/ok-marketplace-app-ktor/src/jvmMain/kotlin/ApplicationJvm.kt +++ b/ok-marketplace-app-ktor/src/jvmMain/kotlin/ApplicationJvm.kt @@ -18,7 +18,10 @@ import org.slf4j.event.Level import ru.otus.otuskotlin.marketplace.api.v1.apiV1Mapper import ru.otus.otuskotlin.marketplace.app.v1.v1Ad import ru.otus.otuskotlin.marketplace.app.v1.v1Offer +import ru.otus.otuskotlin.marketplace.app.v1.wsHandlerV1 +import ru.otus.otuskotlin.marketplace.app.v2.wsHandlerV2 import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor +import java.time.Duration import ru.otus.otuskotlin.marketplace.app.module as commonModule // function with config (application.conf) @@ -26,8 +29,15 @@ fun main(args: Array): Unit = io.ktor.server.cio.EngineMain.main(args) @Suppress("unused") // Referenced in application.conf fun Application.moduleJvm() { + install(WebSockets) { + pingPeriod = Duration.ofSeconds(15) + timeout = Duration.ofSeconds(15) + maxFrameSize = Long.MAX_VALUE + masking = false + } + val processor = MkplAdProcessor() - commonModule(processor) + commonModule(processor, false) install(CachingHeaders) install(DefaultHeaders) @@ -68,6 +78,13 @@ fun Application.moduleJvm() { v1Offer(processor) } + webSocket("/ws/v1") { + wsHandlerV1(processor) + } + webSocket("/ws/v2") { + wsHandlerV2(processor) + } + static("static") { resources("static") } diff --git a/ok-marketplace-app-ktor/src/jvmMain/kotlin/v1/WsController.kt b/ok-marketplace-app-ktor/src/jvmMain/kotlin/v1/WsController.kt new file mode 100644 index 0000000..0a721ca --- /dev/null +++ b/ok-marketplace-app-ktor/src/jvmMain/kotlin/v1/WsController.kt @@ -0,0 +1,64 @@ +package ru.otus.otuskotlin.marketplace.app.v1 + +import com.fasterxml.jackson.module.kotlin.readValue +import io.ktor.websocket.* +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.receiveAsFlow +import ru.otus.otuskotlin.marketplace.api.v1.apiV1Mapper +import ru.otus.otuskotlin.marketplace.api.v1.models.IRequest +import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.addError +import ru.otus.otuskotlin.marketplace.common.helpers.asMkplError +import ru.otus.otuskotlin.marketplace.common.helpers.isUpdatableCommand +import ru.otus.otuskotlin.marketplace.common.models.MkplWorkMode +import ru.otus.otuskotlin.marketplace.mappers.v1.* +import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub + +val sessions = mutableSetOf() + +suspend fun WebSocketSession.wsHandlerV1(processor: MkplAdProcessor) { + sessions.add(this) + + // Handle init request + val ctx = MkplContext() + ctx.workMode = MkplWorkMode.STUB + processor.exec(ctx) + val init = apiV1Mapper.writeValueAsString(ctx.toTransportInit()) + outgoing.send(Frame.Text(init)) + + // Handle flow + incoming.receiveAsFlow().mapNotNull { it -> + val frame = it as? Frame.Text ?: return@mapNotNull + + val jsonStr = frame.readText() + val context = MkplContext() + + // Handle without flow destruction + try { + val request = apiV1Mapper.readValue(jsonStr) + context.fromTransport(request) + processor.exec(context) + + val result = apiV1Mapper.writeValueAsString(context.toTransportAd()) + + // If change request, response is sent to everyone + if (context.isUpdatableCommand()) { + sessions.forEach { + it.send(Frame.Text(result)) + } + } else { + outgoing.send(Frame.Text(result)) + } + } catch (_: ClosedReceiveChannelException) { + sessions.clear() + } catch (t: Throwable) { + context.addError(t.asMkplError()) + + val result = apiV1Mapper.writeValueAsString(context.toTransportInit()) + outgoing.send(Frame.Text(result)) + } + }.collect() +} diff --git a/ok-marketplace-app-ktor/src/jvmTest/kotlin/stubs/V1WebsocketStubTest.kt b/ok-marketplace-app-ktor/src/jvmTest/kotlin/stubs/V1WebsocketStubTest.kt new file mode 100644 index 0000000..3b7c306 --- /dev/null +++ b/ok-marketplace-app-ktor/src/jvmTest/kotlin/stubs/V1WebsocketStubTest.kt @@ -0,0 +1,149 @@ +package ru.otus.otuskotlin.marketplace.app.stubs + +import io.ktor.client.plugins.websocket.* +import io.ktor.server.testing.* +import io.ktor.websocket.* +import kotlinx.coroutines.withTimeout +import ru.otus.otuskotlin.marketplace.api.v1.apiV1Mapper +import ru.otus.otuskotlin.marketplace.api.v1.models.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class V1WebsocketStubTest { + + @Test + fun createStub() { + val request = AdCreateRequest( + requestId = "12345", + ad = AdCreateObject( + title = "Болт", + description = "КРУТЕЙШИЙ", + adType = DealSide.DEMAND, + visibility = AdVisibility.PUBLIC, + ), + debug = AdDebug( + mode = AdRequestDebugMode.STUB, + stub = AdRequestDebugStubs.SUCCESS + ) + ) + + testMethod(request) { + assertEquals("12345", it.requestId) + } + } + + @Test + fun readStub() { + val request = AdReadRequest( + requestId = "12345", + ad = AdReadObject("666"), + debug = AdDebug( + mode = AdRequestDebugMode.STUB, + stub = AdRequestDebugStubs.SUCCESS + ) + ) + + testMethod(request) { + assertEquals("12345", it.requestId) + } + } + + @Test + fun updateStub() { + val request = AdUpdateRequest( + requestId = "12345", + ad = AdUpdateObject( + id = "666", + title = "Болт", + description = "КРУТЕЙШИЙ", + adType = DealSide.DEMAND, + visibility = AdVisibility.PUBLIC, + ), + debug = AdDebug( + mode = AdRequestDebugMode.STUB, + stub = AdRequestDebugStubs.SUCCESS + ) + ) + + testMethod(request) { + assertEquals("12345", it.requestId) + } + } + + @Test + fun deleteStub() { + val request = AdDeleteRequest( + requestId = "12345", + ad = AdDeleteObject( + id = "666", + ), + debug = AdDebug( + mode = AdRequestDebugMode.STUB, + stub = AdRequestDebugStubs.SUCCESS + ) + ) + + testMethod(request) { + assertEquals("12345", it.requestId) + } + } + + @Test + fun searchStub() { + val request = AdSearchRequest( + requestId = "12345", + adFilter = AdSearchFilter(), + debug = AdDebug( + mode = AdRequestDebugMode.STUB, + stub = AdRequestDebugStubs.SUCCESS + ) + ) + + testMethod(request) { + assertEquals("12345", it.requestId) + } + } + + @Test + fun offersStub() { + val request = AdOffersRequest( + requestId = "12345", + ad = AdReadObject( + id = "666", + ), + debug = AdDebug( + mode = AdRequestDebugMode.STUB, + stub = AdRequestDebugStubs.SUCCESS + ) + ) + + testMethod(request) { + assertEquals("12345", it.requestId) + } + } + + private inline fun testMethod( + request: Any, + crossinline assertBlock: (T) -> Unit + ) = testApplication { + val client = createClient { + install(WebSockets) + } + + client.webSocket("/ws/v1") { + withTimeout(3000) { + val incame = incoming.receive() as Frame.Text + val response = apiV1Mapper.readValue(incame.readText(), T::class.java) + assertIs(response) + } + send(Frame.Text(apiV1Mapper.writeValueAsString(request))) + withTimeout(3000) { + val incame = incoming.receive() as Frame.Text + val response = apiV1Mapper.readValue(incame.readText(), T::class.java) + + assertBlock(response) + } + } + } +} diff --git a/ok-marketplace-app-ktor/src/jvmTest/kotlin/stubs/V2WebsocketStubTest.kt b/ok-marketplace-app-ktor/src/jvmTest/kotlin/stubs/V2WebsocketStubTest.kt new file mode 100644 index 0000000..5ceb05e --- /dev/null +++ b/ok-marketplace-app-ktor/src/jvmTest/kotlin/stubs/V2WebsocketStubTest.kt @@ -0,0 +1,152 @@ +//package ru.otus.otuskotlin.marketplace.app.stubs +// +//import io.ktor.client.plugins.websocket.* +//import io.ktor.server.testing.* +//import io.ktor.websocket.* +//import kotlinx.coroutines.withTimeout +//import kotlinx.serialization.decodeFromString +//import kotlinx.serialization.encodeToString +//import ru.otus.otuskotlin.marketplace.api.v2.apiV2Mapper +//import ru.otus.otuskotlin.marketplace.api.v2.models.* +//import kotlin.test.Test +//import kotlin.test.assertEquals +//import kotlin.test.assertIs +// +//class V2WebsocketStubTest { +// +// @Test +// fun createStub() { +// val request = AdCreateRequest( +// requestId = "12345", +// ad = AdCreateObject( +// title = "Болт", +// description = "КРУТЕЙШИЙ", +// adType = DealSide.DEMAND, +// visibility = AdVisibility.PUBLIC, +// ), +// debug = AdDebug( +// mode = AdRequestDebugMode.STUB, +// stub = AdRequestDebugStubs.SUCCESS +// ) +// ) +// +// testMethod(request) { +// assertEquals("12345", it.requestId) +// } +// } +// +// @Test +// fun readStub() { +// val request = AdReadRequest( +// requestId = "12345", +// ad = AdReadObject("666"), +// debug = AdDebug( +// mode = AdRequestDebugMode.STUB, +// stub = AdRequestDebugStubs.SUCCESS +// ) +// ) +// +// testMethod(request) { +// assertEquals("12345", it.requestId) +// } +// } +// +// @Test +// fun updateStub() { +// val request = AdUpdateRequest( +// requestId = "12345", +// ad = AdUpdateObject( +// id = "666", +// title = "Болт", +// description = "КРУТЕЙШИЙ", +// adType = DealSide.DEMAND, +// visibility = AdVisibility.PUBLIC, +// ), +// debug = AdDebug( +// mode = AdRequestDebugMode.STUB, +// stub = AdRequestDebugStubs.SUCCESS +// ) +// ) +// +// testMethod(request) { +// assertEquals("12345", it.requestId) +// } +// } +// +// @Test +// fun deleteStub() { +// val request = AdDeleteRequest( +// requestId = "12345", +// ad = AdDeleteObject( +// id = "666", +// ), +// debug = AdDebug( +// mode = AdRequestDebugMode.STUB, +// stub = AdRequestDebugStubs.SUCCESS +// ) +// ) +// +// testMethod(request) { +// assertEquals("12345", it.requestId) +// } +// } +// +// @Test +// fun searchStub() { +// val request = AdSearchRequest( +// requestId = "12345", +// adFilter = AdSearchFilter(), +// debug = AdDebug( +// mode = AdRequestDebugMode.STUB, +// stub = AdRequestDebugStubs.SUCCESS +// ) +// ) +// +// testMethod(request) { +// assertEquals("12345", it.requestId) +// } +// } +// +// @Test +// fun offersStub() { +// val request = AdOffersRequest( +// requestId = "12345", +// ad = AdReadObject( +// id = "666", +// ), +// debug = AdDebug( +// mode = AdRequestDebugMode.STUB, +// stub = AdRequestDebugStubs.SUCCESS +// ) +// ) +// +// testMethod(request) { +// assertEquals("12345", it.requestId) +// } +// } +// +// private inline fun testMethod( +// request: IRequest, +// crossinline assertBlock: (T) -> Unit +// ) = testApplication { +// val client = createClient { +// install(WebSockets) +// } +// +// client.webSocket("/ws/v2") { +// withTimeout(3000) { +// val incame = incoming.receive() as Frame.Text +// val response = apiV2Mapper.decodeFromString(incame.readText()) +// assertIs(response) +// } +// send(Frame.Text(apiV2Mapper.encodeToString(request))) +// withTimeout(3000) { +// val incame = incoming.receive() as Frame.Text +// val text = incame.readText() +// val response = apiV2Mapper.decodeFromString(text) +// +// assertBlock(response) +// } +// } +// } +//} diff --git a/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/service/MkplAdBlockingProcessor.kt b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/service/MkplAdBlockingProcessor.kt new file mode 100644 index 0000000..c3fc331 --- /dev/null +++ b/ok-marketplace-app-spring/src/main/kotlin/ru/otus/otuskotlin/markeplace/springapp/service/MkplAdBlockingProcessor.kt @@ -0,0 +1,13 @@ +package ru.otus.otuskotlin.markeplace.springapp.service + +import kotlinx.coroutines.runBlocking +import org.springframework.stereotype.Service +import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor +import ru.otus.otuskotlin.marketplace.common.MkplContext + +@Service +class MkplAdBlockingProcessor { + private val processor = MkplAdProcessor() + + fun exec(ctx: MkplContext) = runBlocking { processor.exec(ctx) } +}