Skip to content

Feat: Automated Create Schema #254

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions restdocs-api-spec-mockmvc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ repositories {
}

val springBootVersion: String by extra
val jacksonVersion: String by extra
val springRestDocsVersion: String by extra
val junitVersion: String by extra

Expand All @@ -16,6 +17,9 @@ dependencies {

api(project(":restdocs-api-spec"))
implementation("org.springframework.restdocs:spring-restdocs-mockmvc:$springRestDocsVersion")
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")

testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") {
exclude("junit")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.epages.restdocs.apispec

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler
import org.springframework.restdocs.operation.preprocess.Preprocessors
import org.springframework.restdocs.payload.FieldDescriptor
import org.springframework.restdocs.payload.PayloadDocumentation
import org.springframework.test.web.servlet.ResultActions
import org.springframework.web.servlet.HandlerMapping

object MockMvcAutoRestDocumentationWrapper {
@JvmStatic
fun createDocs(
tag: String,
identifier: String,
description: String,
resultActions: ResultActions
): RestDocumentationResultHandler {
val resourceSnippetParametersBuilder = ResourceSnippetParametersBuilder().tags(tag).description(description)

val request = resultActions.andReturn().request
val requestNode: JsonNode? =
request.contentAsString?.let { jacksonObjectMapper().readTree(request.contentAsString) }
val requestFieldDescriptors = createFieldDescriptors(requestNode)

val response = resultActions.andReturn().response
val responseNode: JsonNode? =
response.contentAsString.let { jacksonObjectMapper().readTree(response.contentAsString) }
val responseFieldDescriptors = createFieldDescriptors(responseNode)

val requestParameter = createParameters(request, ParameterType.QUERY)
val requestPathParameter = createParameters(request, ParameterType.PATH)

resourceSnippetParametersBuilder.requestFields(requestFieldDescriptors)
resourceSnippetParametersBuilder.queryParameters(requestParameter)
resourceSnippetParametersBuilder.pathParameters(requestPathParameter)
resourceSnippetParametersBuilder.responseFields(responseFieldDescriptors)

return MockMvcRestDocumentationWrapper.document(
identifier,
Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
Preprocessors.preprocessResponse(Preprocessors.prettyPrint()),
ResourceDocumentation.resource(resourceSnippetParametersBuilder.build())
)
}

private fun createFieldDescriptors(jsonNode: JsonNode?, parentPath: String = ""): List<FieldDescriptor> {
if (jsonNode == null) return emptyList()

val fieldDescriptors = mutableListOf<FieldDescriptor>()

val iterator = jsonNode.fields()
while (iterator.hasNext()) {
val entry = iterator.next()
val key = entry.key
val value = entry.value
val path = if (parentPath.isEmpty()) key else "$parentPath.$key"

when {
value.isObject -> {
fieldDescriptors.addAll(createFieldDescriptors(value, path))
}

value.isArray -> {
value.forEach { item ->
if (item.isObject) {
fieldDescriptors.addAll(createFieldDescriptors(item, "$path.[]."))
}
}
}

else -> {
fieldDescriptors.add(
PayloadDocumentation.fieldWithPath(path).description(value.asText())
)
}
}
}

return fieldDescriptors
}

private fun createParameters(
request: MockHttpServletRequest,
type: ParameterType
): List<ParameterDescriptorWithType> {
return if (type == ParameterType.QUERY) {
request.parameterMap.map { (key, value) ->
ResourceDocumentation.parameterWithName(key).description(value.joinToString())
}
} else {
val uriVars = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE) as? Map<*, *>

return uriVars?.entries?.map { (key, value) ->
ResourceDocumentation.parameterWithName(key.toString()).description("$value")
}?.toList() ?: listOf()
}
}

private enum class ParameterType {
PATH,
QUERY
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.epages.restdocs.apispec

import com.epages.restdocs.apispec.ResourceSnippetIntegrationTest.TestJoinHolder
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@ExtendWith(SpringExtension::class)
@WebMvcTest
class MockMvcAutoRestDocumentationWrapperTest(@Autowired private val mockMvc: MockMvc) : ResourceSnippetIntegrationTest() {
@Test
fun TestController() {
val testJoinHolder = TestJoinHolder("testLoginId", "123")

val resultActions = mockMvc.perform(
post("/join")
.contentType(MediaType.APPLICATION_JSON)
.content(jacksonObjectMapper().writeValueAsBytes(testJoinHolder))
)

// validate my api response
resultActions.andExpectAll(
status().isOk(),
jsonPath("$.id").value(1),
jsonPath("$.loginId").value("testLoginId"),
jsonPath("$.password").value("123"),
jsonPath("$.createdAt").isNotEmpty
)

// create automated docs
resultActions.andDo(
MockMvcAutoRestDocumentationWrapper.createDocs(
"join",
"join/test",
"join api test",
resultActions
))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RestController
import java.time.OffsetDateTime
import java.util.UUID

@ExtendWith(SpringExtension::class)
Expand Down Expand Up @@ -66,6 +67,22 @@ open class ResourceSnippetIntegrationTest {
.header("X-Custom-Header", customHeader)
.body<EntityModel<TestDataHolder>>(resource)
}

@PostMapping(path = ["/join"])
fun join(
@RequestBody testDataHolder: TestJoinHolder
): ResponseEntity<User> {
val user = User(
1L,
testDataHolder.loginId,
testDataHolder.password,
OffsetDateTime.now()
)

return ResponseEntity
.ok()
.body(user)
}
}
}

Expand All @@ -77,6 +94,18 @@ open class ResourceSnippetIntegrationTest {
@field:NotEmpty
val id: String? = null
)

internal data class TestJoinHolder(
val loginId: String,
val password: String
)

internal class User(
val id: Long,
val loginId: String,
val password: String,
val createdAt: OffsetDateTime
)
}

fun fieldDescriptors(): FieldDescriptors {
Expand Down