Skip to content

Commit

Permalink
m4l1 - OpenAPI (#17)
Browse files Browse the repository at this point in the history
Co-authored-by: Sergey Okatov <sokatov@gmail.com>
  • Loading branch information
evgnep and svok authored Jan 31, 2025
1 parent 1928e35 commit 638161e
Show file tree
Hide file tree
Showing 15 changed files with 1,291 additions and 28 deletions.
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Marketplace -- это площадка, на которой пользовате
1. [Функциональные требования](./docs/02-analysis/01-functional-requiremens.md)
2. [Нефункциональные требования](./docs/02-analysis/02-nonfunctional-requirements.md)
3. DevOps
1. [Файлы сборки](./deploy)
1. [Файлы сборки](./deploy)
4. Архитектура
1. [ADR](docs/03-architecture/01-adrs.md)
2. [Описание API](docs/03-architecture/02-api.md)
Expand All @@ -34,15 +34,26 @@ Marketplace -- это площадка, на которой пользовате
## Подпроекты для занятий по языку Kotlin

1. Модуль 1: Введение в Kotlin
1. [m1l1-first](lessons/m1l1-first) - Вводное занятие, создание первой программы на Kotlin
2. [m1l2-basic](lessons/m1l2-basic) - Основные конструкции Kotlin
3. [m1l3-func](lessons/m1l3-func) - Функциональные элементы Kotlin
4. [m1l4-oop](lessons/m1l4-oop) - Объектно-ориентированное программирование
1. [m1l1-first](lessons/m1l1-first) - Вводное занятие, создание первой программы на Kotlin
2. [m1l2-basic](lessons/m1l2-basic) - Основные конструкции Kotlin
3. [m1l3-func](lessons/m1l3-func) - Функциональные элементы Kotlin
4. [m1l4-oop](lessons/m1l4-oop) - Объектно-ориентированное программирование
2. Модуль 2: Расширенные возможности Kotlin
1. [m2l1-dsl](lessons/m2l1-dsl) - Предметно ориентированные языки (DSL)
1. [m2l1-dsl](lessons/m2l1-dsl) - Предметно ориентированные языки (DSL)
2. [m2l2-coroutines](lessons/m2l2-coroutines) - Асинхронное и многопоточное программирование с корутинами
3. [m2l3-flows](lessons/m2l3-flows) - Асинхронное и многопоточное программирование с Sequence и Flow
3. [m2l3-flows](lessons/m2l3-flows) - Асинхронное и многопоточное программирование с Sequence и Flow
4. [m2l4-kmp](lessons/m2l4-kmp) - Мультиплатформенная разработка
5. m2l5 - Интероперабельность Kotlin с другими языками
5. m2l5 - Интероперабельность Kotlin с другими языками
1. [m2l5-1-interop](lessons/m2l5-1-interop) - Интероперабельность Kotlin Native с C
2. [m2l5-2-jni](lessons/m2l5-2-jni) - Интероперабельность Kotlin JVM с C

## Проектные модули

### Транспортные модели, API

1. [specs](specs) - описание API в форме OpenAPI-спецификаций
2. [ok-marketplace-api-v1-jackson](ok-marketplace-be/ok-marketplace-api-v1-jackson) - Генерация первой версии
транспортных модеелй с Jackson
3. [ok-marketplace-api-v2-kmp](ok-marketplace-be/ok-marketplace-api-v2-kmp) - Генерация второй версии транспортных
моделей с KMP

23 changes: 19 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
[versions]
kotlin = "2.1.0"

kotlinx-datetime = "0.6.1"
kotlinx-serialization = "1.6.3"

binaryCompabilityValidator = "0.13.2"

openapi-generator = "7.3.0"
jackson = "2.16.1"
# BASE
jvm-compiler = "17"
jvm-language = "21"

[libraries]
plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
plugin-binaryCompatibilityValidator = "org.jetbrains.kotlinx:binary-compatibility-validator:0.13.2"
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.1" }
plugin-binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "binaryCompabilityValidator" }
kotlinx-datetime = {module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime"}
kotlinx-serialization-core = {module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization"}
kotlinx-serialization-json = {module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization"}

jackson-kotlin = {module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson"}
jackson-datatype = {module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson"}

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-multiplatform = {id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin"}
kotlin-jvm = {id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"}
openapi-generator = {id = "org.openapi.generator", version.ref = "openapi-generator"}
crowdproj-generator = {id = "com.crowdproj.generator", version = "0.2.0"}
kotlinx-serialization = {id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
23 changes: 11 additions & 12 deletions ok-marketplace-be/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
}

group = "com.otus.otuskotlin.marketplace"
group = "ru.otus.otuskotlin.marketplace"
version = "0.0.1"

allprojects {
Expand All @@ -17,18 +17,17 @@ subprojects {
version = rootProject.version
}

ext {
val specDir = layout.projectDirectory.dir("../specs")
set("spec-v1", specDir.file("specs-ad-v1.yaml").toString())
set("spec-v2", specDir.file("specs-ad-v2.yaml").toString())
}

tasks {
create("build") {
group = "build"
dependsOn(project(":ok-marketplace-tmp").getTasksByName("build",false))
}
create("check") {
group = "verification"
subprojects.forEach { proj ->
println("PROJ $proj")
proj.getTasksByName("check", false).also {
this@create.dependsOn(it)
}
arrayOf("build", "clean", "check").forEach {tsk ->
create(tsk) {
group = "build"
dependsOn(subprojects.map { it.getTasksByName(tsk,false)})
}
}
}
59 changes: 59 additions & 0 deletions ok-marketplace-be/ok-marketplace-api-v1-jackson/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
plugins {
id("build-jvm")
alias(libs.plugins.openapi.generator)
}

sourceSets {
main {
java.srcDir(layout.buildDirectory.dir("generate-resources/main/src/main/kotlin"))
}
}

/**
* Настраиваем генерацию здесь
*/
openApiGenerate {
val openapiGroup = "${rootProject.group}.api.v1"
generatorName.set("kotlin") // Это и есть активный генератор
packageName.set(openapiGroup)
apiPackage.set("$openapiGroup.api")
modelPackage.set("$openapiGroup.models")
invokerPackage.set("$openapiGroup.invoker")
// inputSpec.set("$specDir/specs-ad-v1.yaml")
inputSpec.set(rootProject.ext["spec-v1"] as String)

/**
* Здесь указываем, что нам нужны только модели, все остальное не нужно
* https://openapi-generator.tech/docs/globals
*/
globalProperties.apply {
put("models", "")
put("modelDocs", "false")
}

/**
* Настройка дополнительных параметров из документации по генератору
* https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/kotlin.md
*/
configOptions.set(
mapOf(
"dateLibrary" to "string",
"enumPropertyNaming" to "UPPERCASE",
"serializationLibrary" to "jackson",
"collectionType" to "list"
)
)
}

dependencies {
implementation(kotlin("stdlib"))
implementation(libs.jackson.kotlin)
implementation(libs.jackson.datatype)
testImplementation(kotlin("test-junit"))
}

tasks {
compileKotlin {
dependsOn(openApiGenerate)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ru.otus.otuskotlin.marketplace.api.v1

import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import ru.otus.otuskotlin.marketplace.api.v1.models.IRequest
import ru.otus.otuskotlin.marketplace.api.v1.models.IResponse
val apiV1Mapper = JsonMapper.builder().run {
// configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
enable(MapperFeature.USE_BASE_TYPE_AS_DEFAULT_IMPL)
// setSerializationInclusion(JsonInclude.Include.NON_NULL)
build()
}

@Suppress("unused")
fun apiV1RequestSerialize(request: IRequest): String = apiV1Mapper.writeValueAsString(request)

@Suppress("UNCHECKED_CAST", "unused")
fun <T : IRequest> apiV1RequestDeserialize(json: String): T =
apiV1Mapper.readValue(json, IRequest::class.java) as T

@Suppress("unused")
fun apiV1ResponseSerialize(response: IResponse): String = apiV1Mapper.writeValueAsString(response)

@Suppress("UNCHECKED_CAST", "unused")
fun <T : IResponse> apiV1ResponseDeserialize(json: String): T =
apiV1Mapper.readValue(json, IResponse::class.java) as T
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package ru.otus.otuskotlin.marketplace.api.v1

import ru.otus.otuskotlin.marketplace.api.v1.models.*
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals

class RequestV1SerializationTest {
private val request = AdCreateRequest(
debug = AdDebug(
mode = AdRequestDebugMode.STUB,
stub = AdRequestDebugStubs.BAD_TITLE
),
ad = AdCreateObject(
title = "ad title",
description = "ad description",
adType = DealSide.DEMAND,
visibility = AdVisibility.PUBLIC,
)
)

@Test
fun serialize() {
val json = apiV1Mapper.writeValueAsString(request)

assertContains(json, Regex("\"title\":\\s*\"ad title\""))
assertContains(json, Regex("\"mode\":\\s*\"stub\""))
assertContains(json, Regex("\"stub\":\\s*\"badTitle\""))
assertContains(json, Regex("\"requestType\":\\s*\"create\""))
}

@Test
fun deserialize() {
val json = apiV1Mapper.writeValueAsString(request)
val obj = apiV1Mapper.readValue(json, IRequest::class.java) as AdCreateRequest

assertEquals(request, obj)
}

@Test
fun deserializeNaked() {
val jsonString = """
{"ad": null}
""".trimIndent()
val obj = apiV1Mapper.readValue(jsonString, AdCreateRequest::class.java)

assertEquals(null, obj.ad)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ru.otus.otuskotlin.marketplace.api.v1

import ru.otus.otuskotlin.marketplace.api.v1.models.*
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals

class ResponseV1SerializationTest {
private val response = AdCreateResponse(
ad = AdResponseObject(
title = "ad title",
description = "ad description",
adType = DealSide.DEMAND,
visibility = AdVisibility.PUBLIC,
)
)

@Test
fun serialize() {
val json = apiV1Mapper.writeValueAsString(response)

assertContains(json, Regex("\"title\":\\s*\"ad title\""))
assertContains(json, Regex("\"responseType\":\\s*\"create\""))
}

@Test
fun deserialize() {
val json = apiV1Mapper.writeValueAsString(response)
val obj = apiV1Mapper.readValue(json, IResponse::class.java) as AdCreateResponse

assertEquals(response, obj)
}
}
48 changes: 48 additions & 0 deletions ok-marketplace-be/ok-marketplace-api-v2-kmp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask

plugins {
id("build-kmp")
alias(libs.plugins.crowdproj.generator)
alias(libs.plugins.kotlinx.serialization)
}

crowdprojGenerate {
packageName.set("${project.group}.api.v2")
inputSpec.set(rootProject.ext["spec-v2"] as String)
}

kotlin {
sourceSets {
val serializationVersion: String by project
val commonMain by getting {
kotlin.srcDirs(layout.buildDirectory.dir("generate-resources/src/commonMain/kotlin"))
dependencies {
implementation(kotlin("stdlib-common"))

implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit"))
}
}
}
}

tasks {
val openApiGenerateTask: GenerateTask = getByName("openApiGenerate", GenerateTask::class) {
outputDir.set(layout.buildDirectory.file("generate-resources").get().toString())
finalizedBy("compileCommonMainKotlinMetadata")
}
filter { it.name.startsWith("compile") }.forEach {
it.dependsOn(openApiGenerateTask)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@file:Suppress("unused")

package ru.otus.otuskotlin.marketplace.api.v2

import kotlinx.serialization.json.Json
import ru.otus.otuskotlin.marketplace.api.v2.models.IRequest
import ru.otus.otuskotlin.marketplace.api.v2.models.IResponse

@Suppress("JSON_FORMAT_REDUNDANT_DEFAULT")
val apiV2Mapper = Json {
// ignoreUnknownKeys = true
}

@Suppress("UNCHECKED_CAST")
fun <T : IRequest> apiV2RequestDeserialize(json: String) =
apiV2Mapper.decodeFromString<IRequest>(json) as T

fun apiV2ResponseSerialize(obj: IResponse): String =
apiV2Mapper.encodeToString(IResponse.serializer(), obj)

@Suppress("UNCHECKED_CAST")
fun <T : IResponse> apiV2ResponseDeserialize(json: String) =
apiV2Mapper.decodeFromString<IResponse>(json) as T

@Suppress("unused")
fun apiV2RequestSerialize(obj: IRequest): String =
apiV2Mapper.encodeToString(IRequest.serializer(), obj)
Loading

0 comments on commit 638161e

Please sign in to comment.