Skip to content

ApiDoc-1 RestDoc 적용하기 #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Custom
.idea
**/src/main/resources/static/swagger

# Created by https://www.toptal.com/developers/gitignore/api/kotlin,java,gradle,intellij+all
# Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,java,gradle,intellij+all
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@
./gradlew addKtlintCheckGitPreCommitHook
```

# Swagger

```text
./gradlew copySwaggerUI
```
위 Task를 실행하면 크게 3가지 작업을 진행한다.
1. Test 실행
2. Controller 테스트 실행된 결과 snippet 을 저장
3. snippet을 이용하여 Swagger 생성

Swagger 파일은 src/main/resources/static/swagger 아래에 저장된다.

### References

[RestDoc 세팅](https://techblog.woowahan.com/2597/)
[Swagger 파일 생성](https://jwkim96.tistory.com/274)

# Module

## 헥사고날 아키텍쳐
Expand Down
52 changes: 52 additions & 0 deletions adapter-in/web/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,19 +1,71 @@
import groovy.lang.Closure
import io.swagger.v3.oas.models.servers.Server
import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
id("com.epages.restdocs-api-spec") version "0.18.0"
id("org.hidetake.swagger.generator") version "2.19.2"
}

dependencies {
val springVersion by properties

implementation(project(":support:yaml"))
implementation(project(":core"))

implementation("org.springframework.boot:spring-boot-starter-web:$springVersion")

testImplementation("org.springframework.boot:spring-boot-starter-test:$springVersion")
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc:3.0.0")
testImplementation("com.epages:restdocs-api-spec-mockmvc:0.18.0")
swaggerCodegen("io.swagger.codegen.v3:swagger-codegen-cli:3.0.42")
swaggerUI("org.webjars:swagger-ui:4.18.2")
}

tasks {
withType<Jar> { enabled = false }
withType<BootJar> {
enabled = true
dependsOn("copySwaggerUI")
mainClass.set("yapp.rating.boardgame.WebApplicationKt")
}

val generateSwaggerUIPrefix = "Api"
openapi3 {
setServers(
listOf(
toServer("http://localhost:8080"),
)
)
title = "My API"
description = "My API description"
// tagDescriptionsPropertiesFile = "src/docs/tag-descriptions.yaml"
version = "0.1.0"
format = "yaml"
}
postman {
title = "My API"
version = "0.1.0"
baseUrl = "https://localhost:8080"
}
swaggerSources {
create(generateSwaggerUIPrefix).apply {
setInputFile(file("${project.buildDir}/api-spec/openapi3.yaml"))
}
}
withType<GenerateSwaggerUI> {
dependsOn("openapi3")
}
register<Copy>("copySwaggerUI") {
dependsOn("generateSwaggerUI$generateSwaggerUIPrefix")

val generateSwaggerUISampleTask =
[email protected]<GenerateSwaggerUI>("generateSwaggerUI$generateSwaggerUIPrefix").get()

from("${generateSwaggerUISampleTask.outputDir}")
into("src/main/resources/static/swagger")
}
}

fun toServer(url: String): Closure<Server> = closureOf<Server> { this.url = url } as Closure<Server>
20 changes: 20 additions & 0 deletions adapter-in/web/src/main/kotlin/yapp/rating/TestController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package yapp.rating

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

// TODO: Delete 테스트 코드 전용 클래스
@RestController
@RequestMapping("/v1/test")
class TestController {

@GetMapping
fun testGet(): TestResponse {
return TestResponse("Good!")
}

data class TestResponse(
val value: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package yapp.rating.boardgame
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
@SpringBootApplication(scanBasePackages = ["yapp.rating"])
open class WebApplication

fun main(args: Array<String>) {
Expand Down
51 changes: 51 additions & 0 deletions adapter-in/web/src/test/kotlin/yapp/rating/ControllerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package yapp.rating

import com.fasterxml.jackson.databind.ObjectMapper
import io.kotest.core.spec.style.FunSpec
import org.springframework.restdocs.ManualRestDocumentation
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
import org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder

// @ExtendWith(RestDocumentationExtension::class)
abstract class ControllerTest : FunSpec() {
protected abstract val controller: Any
protected lateinit var mockMvc: MockMvc
private set

private val restDocumentation = ManualRestDocumentation()

init {
beforeSpec {
setUpMockMvc()
}
beforeEach {
restDocumentation.beforeTest(javaClass, it.name.testName)
}
afterEach {
restDocumentation.afterTest()
}
}

private fun setUpMockMvc() {
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.apply<StandaloneMockMvcBuilder>(
MockMvcRestDocumentation.documentationConfiguration(restDocumentation)
.uris().withScheme("http").withHost("localhost").and()
.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint())
)
// .setControllerAdvice(ExceptionHandler())
// .setCustomArgumentResolvers()
// .setMessageConverters()
Comment on lines +41 to +43
Copy link
Contributor Author

Choose a reason for hiding this comment

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

추후에 필요한 값들이라 주석으로 명시해뒀습니다.

.build()
}

companion object {
@JvmStatic
protected val objectMapper = ObjectMapper()
}
}
19 changes: 19 additions & 0 deletions adapter-in/web/src/test/kotlin/yapp/rating/TestControllerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package yapp.rating

import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

class TestControllerTest : ControllerTest() {
override val controller = TestController()

init {
test("Get Test") {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

여기서 RestDoc의 Kotlin DSL 을 사용 하면 Spring 과 호환이 되지 않아 정상적으로 작동하지 않습니다.

호환성 이슈 관련

참고 Kotlin DSL 은 아래와 같은 코드를 말합니다.

.andExpect { status().isOK }

mockMvc.perform(get("/v1/test"))
.andExpect(status().isOk)
.andDo(
document("test")
)
}
}
}
4 changes: 1 addition & 3 deletions adapter-out/rdb/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@ dependencies {
api(project(":port-out"))

implementation("org.springframework.boot:spring-boot-starter-data-jpa:$springVersion")

// DB connect
runtimeOnly("com.h2database:h2")
runtimeOnly("mysql:mysql-connector-java:8.0.33")

testImplementation("org.springframework.boot:spring-boot-starter-test:$springVersion")
testRuntimeOnly("com.h2database:h2")
}

tasks {
Expand Down
22 changes: 12 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
kotlin("jvm") version "1.7.22"
kotlin("kapt") version "1.7.22"
id("org.jlleitschuh.gradle.ktlint") version "10.2.0"

id("io.spring.dependency-management") version "1.1.0"

id("org.springframework.boot")
}

Expand All @@ -33,18 +32,21 @@ subprojects {
dependencies {
val kotestVersion: String by properties

implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.0")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Kotlin 에서는 Jackson이 정상적으로 작동하지 않아 추가했습니다.

Jackson은 기본 생성자를 필요로 하는데
Kotlin Data class 에서 문제가 발생합니다.
해당 의존성을 추가하면 해결됩니다.


testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion")
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
tasks {
withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
test {
useJUnitPlatform()
}
}

tasks.withType<Test> {
useJUnitPlatform()
}
}