diff --git a/.github/workflows/auto-assign-workflow.yaml b/.github/workflows/auto-assign-workflow.yaml deleted file mode 100644 index d9feafa..0000000 --- a/.github/workflows/auto-assign-workflow.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: Auto Assign -on: - pull_request_target: - types: [opened, ready_for_review] - -jobs: - add-reviews: - runs-on: ubuntu-latest - steps: - - uses: kentaro-m/auto-assign-action@v1.2.0 diff --git a/example/build.gradle.kts b/example/build.gradle.kts index 538e80b..7defdb2 100644 --- a/example/build.gradle.kts +++ b/example/build.gradle.kts @@ -11,6 +11,7 @@ val snippetsDir by extra { file("build/generated-snippets") } dependencies { implementation(projects.mockmvc) implementation(libs.spring.boot.starter.web) + implementation(libs.spring.boot.starter.validation) testImplementation(libs.spring.boot.starter.test) diff --git a/example/src/main/kotlin/io/github/lcomment/example/Application.kt b/example/src/main/kotlin/io/github/lcomment/example/Application.kt index c5b9cf7..aabc64e 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/Application.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/Application.kt @@ -1,3 +1,21 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example import jakarta.annotation.PostConstruct diff --git a/example/src/main/kotlin/io/github/lcomment/example/controller/ExampleController.kt b/example/src/main/kotlin/io/github/lcomment/example/controller/ExampleController.kt index e9793cd..d2deeda 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/controller/ExampleController.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/controller/ExampleController.kt @@ -1,10 +1,35 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.controller +import io.github.lcomment.example.dto.request.ExampleMultipartRequest +import io.github.lcomment.example.dto.request.ExampleRequest import io.github.lcomment.example.dto.response.ApiResponse import io.github.lcomment.example.dto.response.ExampleResponse import io.github.lcomment.example.service.ExampleService +import jakarta.validation.Valid +import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController @RestController @@ -20,4 +45,26 @@ class ExampleController( return ApiResponse.success(data) } + + @PostMapping("/example/{id}") + fun post( + @PathVariable id: Long, + @RequestBody @Valid request: ExampleRequest, + ): ApiResponse { + exampleService.post(request) + + return ApiResponse.success() + } + + @PostMapping( + "/example", + consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun saveImages( + @ModelAttribute @Valid request: ExampleMultipartRequest, +// @RequestParam message: String, + ): ApiResponse { + return ApiResponse.success() + } } diff --git a/example/src/main/kotlin/io/github/lcomment/example/dto/request/ExampleMultipartRequest.kt b/example/src/main/kotlin/io/github/lcomment/example/dto/request/ExampleMultipartRequest.kt new file mode 100644 index 0000000..9e08a9b --- /dev/null +++ b/example/src/main/kotlin/io/github/lcomment/example/dto/request/ExampleMultipartRequest.kt @@ -0,0 +1,10 @@ +package io.github.lcomment.example.dto.request + +import jakarta.validation.constraints.NotBlank +import org.springframework.web.multipart.MultipartFile + +data class ExampleMultipartRequest( + val images: List = emptyList(), + @field:NotBlank + var message: String? = null, +) diff --git a/example/src/main/kotlin/io/github/lcomment/example/dto/request/ExampleRequest.kt b/example/src/main/kotlin/io/github/lcomment/example/dto/request/ExampleRequest.kt new file mode 100644 index 0000000..9121565 --- /dev/null +++ b/example/src/main/kotlin/io/github/lcomment/example/dto/request/ExampleRequest.kt @@ -0,0 +1,26 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.example.dto.request + +import jakarta.validation.constraints.NotBlank + +data class ExampleRequest( + @field:NotBlank + val example: String? = null, +) diff --git a/example/src/main/kotlin/io/github/lcomment/example/dto/response/ApiResponse.kt b/example/src/main/kotlin/io/github/lcomment/example/dto/response/ApiResponse.kt index e76b1e6..509ebe7 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/dto/response/ApiResponse.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/dto/response/ApiResponse.kt @@ -1,7 +1,27 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.dto.response import io.github.lcomment.example.enums.ReturnCode +import com.fasterxml.jackson.annotation.JsonInclude +@JsonInclude(JsonInclude.Include.NON_NULL) data class ApiResponse( val code: String, val message: String, diff --git a/example/src/main/kotlin/io/github/lcomment/example/dto/response/ArrayResponse.kt b/example/src/main/kotlin/io/github/lcomment/example/dto/response/ArrayResponse.kt index 419fd1e..596a1be 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/dto/response/ArrayResponse.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/dto/response/ArrayResponse.kt @@ -1,3 +1,21 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.dto.response data class ArrayResponse( diff --git a/example/src/main/kotlin/io/github/lcomment/example/dto/response/DateResponse.kt b/example/src/main/kotlin/io/github/lcomment/example/dto/response/DateResponse.kt index 899e2f0..442a6e5 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/dto/response/DateResponse.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/dto/response/DateResponse.kt @@ -1,3 +1,21 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.dto.response import java.time.LocalDate diff --git a/example/src/main/kotlin/io/github/lcomment/example/dto/response/ExampleResponse.kt b/example/src/main/kotlin/io/github/lcomment/example/dto/response/ExampleResponse.kt index 142ad52..c263bf1 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/dto/response/ExampleResponse.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/dto/response/ExampleResponse.kt @@ -1,3 +1,21 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.dto.response import io.github.lcomment.example.enums.ExampleEnum diff --git a/example/src/main/kotlin/io/github/lcomment/example/dto/response/NumberResponse.kt b/example/src/main/kotlin/io/github/lcomment/example/dto/response/NumberResponse.kt index 3a0c3ad..db3fb3b 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/dto/response/NumberResponse.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/dto/response/NumberResponse.kt @@ -1,3 +1,21 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.dto.response data class NumberResponse( diff --git a/example/src/main/kotlin/io/github/lcomment/example/enums/ExampleEnum.kt b/example/src/main/kotlin/io/github/lcomment/example/enums/ExampleEnum.kt index 42271b0..c2dee6e 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/enums/ExampleEnum.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/enums/ExampleEnum.kt @@ -1,3 +1,21 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.enums enum class ExampleEnum { diff --git a/example/src/main/kotlin/io/github/lcomment/example/enums/ReturnCode.kt b/example/src/main/kotlin/io/github/lcomment/example/enums/ReturnCode.kt index 53fc932..bf7f21e 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/enums/ReturnCode.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/enums/ReturnCode.kt @@ -1,3 +1,21 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.enums enum class ReturnCode( diff --git a/example/src/main/kotlin/io/github/lcomment/example/service/ExampleService.kt b/example/src/main/kotlin/io/github/lcomment/example/service/ExampleService.kt index 2dff5ba..cdbabd1 100644 --- a/example/src/main/kotlin/io/github/lcomment/example/service/ExampleService.kt +++ b/example/src/main/kotlin/io/github/lcomment/example/service/ExampleService.kt @@ -1,5 +1,24 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.service +import io.github.lcomment.example.dto.request.ExampleRequest import io.github.lcomment.example.dto.response.ExampleResponse import org.springframework.stereotype.Service @@ -9,4 +28,10 @@ class ExampleService { fun get(): ExampleResponse { return ExampleResponse() } + + fun post( + request: ExampleRequest, + ) { + // do nothing + } } diff --git a/example/src/test/kotlin/io/github/lcomment/example/controller/ApiSpec.kt b/example/src/test/kotlin/io/github/lcomment/example/controller/ApiSpec.kt index 849aaa1..7dd73f8 100644 --- a/example/src/test/kotlin/io/github/lcomment/example/controller/ApiSpec.kt +++ b/example/src/test/kotlin/io/github/lcomment/example/controller/ApiSpec.kt @@ -1,61 +1,86 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.github.lcomment.example.controller +import io.github.lcomment.example.dto.request.ExampleMultipartRequest +import io.github.lcomment.example.dto.request.ExampleRequest import io.github.lcomment.example.dto.response.ExampleResponse import io.github.lcomment.example.enums.ExampleEnum import io.github.lcomment.example.enums.ReturnCode import io.github.lcomment.example.service.ExampleService -import io.github.lcomment.korestdocs.mockmvc.andDocument -import io.github.lcomment.korestdocs.mockmvc.requestWithDocs +import io.github.lcomment.korestdocs.mockmvc.KorestDocumentationExtension +import io.github.lcomment.korestdocs.mockmvc.documentation +import io.github.lcomment.korestdocs.mockmvc.extensions.andDocument +import io.github.lcomment.korestdocs.mockmvc.extensions.requestWithDocs import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mockito.doReturn -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.mock.web.MockMultipartFile import org.springframework.restdocs.RestDocumentationContextProvider -import org.springframework.restdocs.RestDocumentationExtension import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.multipart import org.springframework.test.web.servlet.result.MockMvcResultHandlers -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.web.context.WebApplicationContext import org.springframework.web.filter.CharacterEncodingFilter +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -@WebMvcTest(ExampleController::class) -@ExtendWith(RestDocumentationExtension::class) -class ApiSpec( - @Autowired - private val context: WebApplicationContext, -) { +@SpringBootTest +@ExtendWith(KorestDocumentationExtension::class) +internal class ApiSpec { - @Autowired lateinit var mockMvc: MockMvc @MockBean lateinit var exampleService: ExampleService @BeforeEach - fun setup(provider: RestDocumentationContextProvider) { + fun setup( + provider: RestDocumentationContextProvider, + context: WebApplicationContext, + ) { this.mockMvc = buildRestdocsMockMvc(context, provider) } @Test - fun `test www`() { + fun `extension function - GET`() { val data = ExampleResponse() doReturn(data).`when`(exampleService).get() - mockMvc.requestWithDocs(HttpMethod.GET, "/example/{id}", 1, dsl = { + mockMvc.requestWithDocs(HttpMethod.GET, "/example/{id}", 1) { header(HttpHeaders.AUTHORIZATION, "Bearer access-token") param("param1", "value1") - }) - .andExpect { status().isOk } - .andDo { print() } - .andDocument("example") { + } + .andExpect { + status { isOk() } + } + .andDocument("junit-get-extension-function") { requestHeader { header("Authorization", "Access Token", "Bearer access-token") } @@ -96,6 +121,139 @@ class ApiSpec( } } } + + @Test + fun `extension function - POST`() { + val request = ExampleRequest("example") + + mockMvc.requestWithDocs(HttpMethod.POST, "/example/{id}", 1) { + header(HttpHeaders.AUTHORIZATION, "Bearer access-token") + content = toJson(request) + contentType = MediaType.APPLICATION_JSON + } + .andExpect { status { isOk() } } + .andDocument("junit-post-extension-function") { + requestHeader { + header("Authorization", "Access Token", "Bearer access-token") + } + + requestField { + field("example", "example request", "example") + } + } + } + + @Test + fun `extension function - MULTIPART POST`() { + val image1 = MockMultipartFile("images", "", "image/png", "image1".toByteArray()) + val image2 = MockMultipartFile("images", "", "image/png", "image2".toByteArray()) + val request = ExampleMultipartRequest(listOf(image1, image2)) + + mockMvc.multipart(HttpMethod.POST, "/example") { + header(HttpHeaders.AUTHORIZATION, "Bearer access-token") + file(image1) + file(image2) + param("message", "exampleMessage") + contentType = MediaType.MULTIPART_FORM_DATA + accept = MediaType.APPLICATION_JSON + with { + it.method = HttpMethod.POST.name() + it + } + } + .andExpect { status { isOk() } } + .andDocument("junit-multipart-post-extension-function") { + requestHeader { + header("Authorization", "Access Token", "Bearer access-token") + } + + requestPart { + part("images", "Images", image1) + part("images", "Images", image2) + } + } + } + + @Test + fun `function - GET`() { + val data = ExampleResponse() + doReturn(data).`when`(exampleService).get() + + documentation("junit-get-function") { + request(HttpMethod.GET, "/example/{id}") { + pathVariable("id", "아이디", 1) + } + + requestParameter { + queryParameter("param1", "파라미터 1", "value1") + } + + responseField { + field("code", "Response Code", ReturnCode.SUCCESS.code) + field("message", "Response Message", ReturnCode.SUCCESS.message) + field("data", "Response Data", data) + field("data.booleanData", "Boolean Data", data.booleanData) + optionalField("data.stringData", "String Data", data.stringData) + field("data.numberData", "Number Data", data.numberData) + optionalField("data.numberData.number1", "Integer Data", data.numberData.number1) + field("data.numberData.number2", "Long Data", data.numberData.number2) + field("data.numberData.number3", "Short Data", data.numberData.number3) + field("data.numberData.number4", "Float Data", data.numberData.number4) + field("data.numberData.number5", "Double Data", data.numberData.number5) + field("data.arrayData", "Array Data", data.arrayData) + optionalField("data.arrayData.array1", "Array Data", data.arrayData.array1) + field("data.arrayData.array2", "List Data", data.arrayData.array2) + field("data.arrayData.array3", "Set Data", data.arrayData.array3) + field("data.enumData", "Enum Data", ExampleEnum.EXAMPLE1) + field("data.dateData", "Date Data", data.DateData) + field("data.dateData.date", "Date Data", data.DateData.date) + field("data.dateData.time", "Time Data", data.DateData.time) + optionalField("data.dateData.dateTime1", "DateTime Data", data.DateData.dateTime1) + field("data.dateData.dateTime2", "DateTime Data", data.DateData.dateTime2) + field("data.dateData.dateTime3", "DateTime Data", data.DateData.dateTime3) + field("data.dateData.dateTime4", "DateTime Data", data.DateData.dateTime4) + field("data.dateData.dateTime5", "DateTime Data", data.DateData.dateTime5) + } + } + } + + @Test + fun `function - POST`() { + documentation("junit-post-function") { + request(HttpMethod.POST, "/example/{id}") { + pathVariable("id", "아이디", 1) + } + + requestHeader { + header("Authorization", "Access Token", "Bearer access-token") + } + + requestField { + field("example", "example request", "example") + } + + responseField { + field("code", "Response Code", ReturnCode.SUCCESS.code) + field("message", "Response Message", ReturnCode.SUCCESS.message) + } + } + } + + @Test + fun `function - MULTIPART POST`() { + val image1 = MockMultipartFile("images", "", "image/png", "image1".toByteArray()) + val image2 = MockMultipartFile("images", "", "image/png", "image2".toByteArray()) + val request = ExampleMultipartRequest(listOf(image1, image2)) + + documentation("junit-multipart-post-function") { + multipart(HttpMethod.POST, "/example") + + requestPart { + part("images", "Images", image1) + part("images", "Images", image2) + } + } + } } fun buildRestdocsMockMvc( @@ -109,3 +267,11 @@ fun buildRestdocsMockMvc( .alwaysDo(MockMvcResultHandlers.print()) .build() } + +fun toJson(value: Any): String { + return mapper.writeValueAsString(value) +} + +private val mapper: ObjectMapper = ObjectMapper() + .registerModule(JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) diff --git a/gradle.properties b/gradle.properties index 9f59f1b..773a2f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ ### OpenSource Configs ### -version=0.1.0 +version=1.0.0 artifact=korest-docs group=io.github.lcomment ### Deployment Secrets ### @@ -8,5 +8,3 @@ mavenCentralPassword=PASSWORD_TOKEN signing.keyId=GPG_PUBLIC_KEY signing.password=GPG_PRIVATE_KEY_PASSWORD signing.secretKeyRingFile=SECRET_KEY_PATH - - diff --git a/korest-docs-core/build.gradle.kts b/korest-docs-core/build.gradle.kts index 72037e6..0dd11b7 100644 --- a/korest-docs-core/build.gradle.kts +++ b/korest-docs-core/build.gradle.kts @@ -1,6 +1,16 @@ +import com.vanniktech.maven.publish.SonatypeHost + +plugins { + alias(libs.plugins.vanniktech.maven.publish) +} + dependencies { implementation(libs.restdocs.core) implementation(libs.spring.web) + implementation(libs.spring.test) + + testImplementation(libs.kotest.junit) + testImplementation(libs.kotest.assertions.core) } tasks.bootJar { @@ -10,3 +20,43 @@ tasks.bootJar { tasks.jar { enabled = true } + +mavenPublishing { + coordinates( + groupId = "${property("group")}", + artifactId = "${property("artifact")}-core", + version = "${property("version")}" + ) + + pom { + name.set("korest-docs") + description.set("Spring Restdocs extension library using Kotlin Dsl") + inceptionYear.set("2025") + url.set("https://github.com/lcomment/korest-docs") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + + developers { + developer { + id.set("lcomment") + name.set("Hyunseok Ko") + email.set("komment.dev@gmail.com") + } + } + + scm { + connection.set("scm:git:git://github.com/lcomment/korest-docs.git") + developerConnection.set("scm:git:ssh://github.com/lcomment/korest-docs.git") + url.set("https://github.com/lcomment/korest-docs.git") + } + } + + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + + signAllPublications() +} diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extension/KClassExtension.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extensions/KClassExtensions.kt similarity index 97% rename from korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extension/KClassExtension.kt rename to korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extensions/KClassExtensions.kt index 836e55c..956b1e9 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extension/KClassExtension.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extensions/KClassExtensions.kt @@ -16,7 +16,7 @@ * limitations under the License. */ -package io.github.lcomment.korestdocs.extension +package io.github.lcomment.korestdocs.extensions import io.github.lcomment.korestdocs.type.AnyField import io.github.lcomment.korestdocs.type.ArrayField diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extension/MapExtension.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extensions/MapExtensions.kt similarity index 97% rename from korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extension/MapExtension.kt rename to korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extensions/MapExtensions.kt index 23b92cd..40e99e7 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extension/MapExtension.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/extensions/MapExtensions.kt @@ -16,7 +16,7 @@ * limitations under the License. */ -package io.github.lcomment.korestdocs.extension +package io.github.lcomment.korestdocs.extensions import io.github.lcomment.korestdocs.type.DateField import io.github.lcomment.korestdocs.type.DateTimeField diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/DocumentSpec.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/DocumentSpec.kt index 2336751..0bdf019 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/DocumentSpec.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/DocumentSpec.kt @@ -19,95 +19,6 @@ package io.github.lcomment.korestdocs.spec import io.github.lcomment.korestdocs.annotation.RestdocsSpecDslMarker -import org.springframework.restdocs.hypermedia.LinkExtractor -import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor -import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor -import org.springframework.restdocs.payload.PayloadSubsectionExtractor -import org.springframework.restdocs.snippet.Snippet @RestdocsSpecDslMarker -interface DocumentSpec { - - val identifier: String - - val requestPreprocessor: OperationRequestPreprocessor - - val responsePreprocessor: OperationResponsePreprocessor - - val snippets: List - - fun addSnippet(snippet: Snippet) - - fun pathParameter( - relaxed: Boolean = false, - attributes: Map = emptyMap(), - configure: ParametersSpec.() -> Unit, - ) - - fun requestHeader( - attributes: Map = emptyMap(), - configure: HeadersSpec.() -> Unit, - ) - - fun requestParameter( - relaxed: Boolean = false, - attributes: Map = emptyMap(), - configure: ParametersSpec.() -> Unit, - ) - - fun requestField( - relaxed: Boolean = false, - subsectionExtractor: PayloadSubsectionExtractor<*>? = null, - attributes: Map = emptyMap(), - configure: FieldsSpec.() -> Unit, - ) - - fun responseField( - relaxed: Boolean = false, - subsectionExtractor: PayloadSubsectionExtractor<*>? = null, - attributes: Map = emptyMap(), - configure: FieldsSpec.() -> Unit, - ) - - fun requestPart( - relaxed: Boolean = false, - attributes: Map = emptyMap(), - configure: RequestPartsSpec.() -> Unit, - ) - - fun requestPartBody( - part: String, - subsectionExtractor: PayloadSubsectionExtractor<*>? = null, - attributes: Map = emptyMap(), - ) - - fun requestPartField( - part: String, - relaxed: Boolean = false, - subsectionExtractor: PayloadSubsectionExtractor<*>? = null, - attributes: Map = emptyMap(), - configure: FieldsSpec.() -> Unit, - ) - - fun requestBody( - subsectionExtractor: PayloadSubsectionExtractor<*>? = null, - attributes: Map = emptyMap(), - ) - - fun responseHeader( - attributes: Map = emptyMap(), - configure: HeadersSpec.() -> Unit, - ) - - fun responseBody( - subsectionExtractor: PayloadSubsectionExtractor<*>? = null, - attributes: Map = emptyMap(), - ) - - fun link( - relaxed: Boolean = false, - linkExtractor: LinkExtractor? = null, - attributes: Map = emptyMap(), - configure: LinksSpec.() -> Unit, - ) -} +interface DocumentSpec diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/DocumentSpecBuilder.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/DocumentSpecBuilder.kt deleted file mode 100644 index f49878c..0000000 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/DocumentSpecBuilder.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Korest Docs - * - * Copyright 2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.github.lcomment.korestdocs.spec - -import org.springframework.restdocs.hypermedia.LinkExtractor -import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor -import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor -import org.springframework.restdocs.payload.PayloadDocumentation -import org.springframework.restdocs.payload.PayloadSubsectionExtractor -import org.springframework.restdocs.snippet.Snippet - -internal class DocumentSpecBuilder( - override val identifier: String, - override val requestPreprocessor: OperationRequestPreprocessor = OperationRequestPreprocessor { r -> r }, - override val responsePreprocessor: OperationResponsePreprocessor = OperationResponsePreprocessor { r -> r }, - override val snippets: MutableList = mutableListOf(), -) : DocumentSpec { - - override fun addSnippet(snippet: Snippet) { - snippets.add(snippet) - } - - override fun pathParameter( - relaxed: Boolean, - attributes: Map, - configure: ParametersSpec.() -> Unit, - ) { - val snippet = ParametersSpecBuilder().apply(configure).buildPathParameters(relaxed, attributes) - - addSnippet(snippet) - } - - override fun requestParameter( - relaxed: Boolean, - attributes: Map, - configure: ParametersSpec.() -> Unit, - ) { - val snippet = ParametersSpecBuilder() - .apply(configure) - .buildQueryParameters(relaxed, attributes) - - addSnippet(snippet) - } - - override fun requestField( - relaxed: Boolean, - subsectionExtractor: PayloadSubsectionExtractor<*>?, - attributes: Map, - configure: FieldsSpec.() -> Unit, - ) { - val snippet = FieldsSpecBuilder() - .apply(configure) - .buildRequestFields(relaxed, subsectionExtractor, attributes) - - addSnippet(snippet) - } - - override fun responseField( - relaxed: Boolean, - subsectionExtractor: PayloadSubsectionExtractor<*>?, - attributes: Map, - configure: FieldsSpec.() -> Unit, - ) { - val snippet = FieldsSpecBuilder() - .apply(configure) - .buildResponseFields(relaxed, subsectionExtractor, attributes) - - addSnippet(snippet) - } - - override fun requestPart( - relaxed: Boolean, - attributes: Map, - configure: RequestPartsSpec.() -> Unit, - ) { - val snippet = RequestPartsSpecBuilder() - .apply(configure) - .build(relaxed, attributes) - - addSnippet(snippet) - } - - override fun requestPartField( - part: String, - relaxed: Boolean, - subsectionExtractor: PayloadSubsectionExtractor<*>?, - attributes: Map, - configure: FieldsSpec.() -> Unit, - ) { - val snippet = FieldsSpecBuilder() - .apply(configure) - .buildRequestPartFields(part, relaxed, subsectionExtractor, attributes) - - addSnippet(snippet) - } - - override fun requestBody( - subsectionExtractor: PayloadSubsectionExtractor<*>?, - attributes: Map, - ) { - val snippet = PayloadDocumentation.requestBody(subsectionExtractor, attributes) - - addSnippet(snippet) - } - - override fun responseBody( - subsectionExtractor: PayloadSubsectionExtractor<*>?, - attributes: Map, - ) { - val snippet = PayloadDocumentation.responseBody(subsectionExtractor, attributes) - - addSnippet(snippet) - } - - override fun requestPartBody( - part: String, - subsectionExtractor: PayloadSubsectionExtractor<*>?, - attributes: Map, - ) { - val snippet = PayloadDocumentation.requestPartBody(part, subsectionExtractor, attributes) - - addSnippet(snippet) - } - - override fun requestHeader( - attributes: Map, - configure: HeadersSpec.() -> Unit, - ) { - val snippet = HeadersSpecBuilder() - .apply(configure) - .buildRequestHeaders(attributes) - - addSnippet(snippet) - } - - override fun responseHeader( - attributes: Map, - configure: HeadersSpec.() -> Unit, - ) { - val snippet = HeadersSpecBuilder() - .apply(configure) - .buildResponseHeaders(attributes) - - addSnippet(snippet) - } - - override fun link( - relaxed: Boolean, - linkExtractor: LinkExtractor?, - attributes: Map, - configure: LinksSpec.() -> Unit, - ) { - val snippet = LinksSpecBuilder() - .apply(configure) - .build(relaxed, linkExtractor, attributes) - - addSnippet(snippet) - } -} - -fun documentationScope(identifier: String): DocumentSpec = DocumentSpecBuilder(identifier) diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpec.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpec.kt index 46ba8be..76baf39 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpec.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpec.kt @@ -23,7 +23,9 @@ import kotlin.reflect.KClass import org.springframework.restdocs.payload.FieldDescriptor @RestdocsSpecDslMarker -abstract class FieldsSpec { +abstract class FieldsSpec : DocumentSpec { + + val fields = mutableMapOf() inline fun field( path: String, diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpecBuilder.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpecBuilder.kt index fc364cd..5180012 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpecBuilder.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpecBuilder.kt @@ -18,9 +18,9 @@ package io.github.lcomment.korestdocs.spec -import io.github.lcomment.korestdocs.extension.putFormat -import io.github.lcomment.korestdocs.extension.toAttributes -import io.github.lcomment.korestdocs.extension.toFieldType +import io.github.lcomment.korestdocs.extensions.putFormat +import io.github.lcomment.korestdocs.extensions.toAttributes +import io.github.lcomment.korestdocs.extensions.toFieldType import kotlin.reflect.KClass import org.springframework.restdocs.payload.FieldDescriptor import org.springframework.restdocs.payload.PayloadDocumentation @@ -29,8 +29,8 @@ import org.springframework.restdocs.payload.RequestFieldsSnippet import org.springframework.restdocs.payload.RequestPartFieldsSnippet import org.springframework.restdocs.payload.ResponseFieldsSnippet -internal class FieldsSpecBuilder( - private val fields: MutableList = mutableListOf(), +class FieldsSpecBuilder( + private val fieldDescriptors: MutableList = mutableListOf(), ) : FieldsSpec() { override fun field( @@ -40,6 +40,7 @@ internal class FieldsSpecBuilder( type: KClass, attributes: Map, ) { + fields.putIfAbsent(path, example) val descriptor = PayloadDocumentation.fieldWithPath(path) .type(type.toFieldType().toString()) .description(description) @@ -64,14 +65,14 @@ internal class FieldsSpecBuilder( } override fun add(fieldDescriptor: FieldDescriptor) { - fields.add(fieldDescriptor) + fieldDescriptors.add(fieldDescriptor) } override fun withPrefix( prefix: String, configure: FieldsSpec.() -> Unit, ) { - val fields = FieldsSpecBuilder().apply(configure).fields + val fields = FieldsSpecBuilder().apply(configure).fieldDescriptors PayloadDocumentation.applyPathPrefix(prefix, fields).forEach(::add) } @@ -84,13 +85,13 @@ internal class FieldsSpecBuilder( PayloadDocumentation.relaxedRequestFields( subsectionExtractor, attributes, - fields, + fieldDescriptors, ) } else { PayloadDocumentation.requestFields( subsectionExtractor, attributes, - fields, + fieldDescriptors, ) } @@ -103,13 +104,13 @@ internal class FieldsSpecBuilder( PayloadDocumentation.relaxedResponseFields( subsectionExtractor, attributes, - fields, + fieldDescriptors, ) } else { PayloadDocumentation.responseFields( subsectionExtractor, attributes, - fields, + fieldDescriptors, ) } @@ -124,14 +125,14 @@ internal class FieldsSpecBuilder( part, subsectionExtractor, attributes, - fields, + fieldDescriptors, ) } else { PayloadDocumentation.requestPartFields( part, subsectionExtractor, attributes, - fields, + fieldDescriptors, ) } } diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpec.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpec.kt index a89af36..32c9878 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpec.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpec.kt @@ -23,7 +23,9 @@ import kotlin.reflect.KClass import org.springframework.restdocs.headers.HeaderDescriptor @RestdocsSpecDslMarker -abstract class HeadersSpec { +abstract class HeadersSpec : DocumentSpec { + + val headers: MutableMap = mutableMapOf() inline fun header( name: String, diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpecBuilder.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpecBuilder.kt index 37cf23f..1105e13 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpecBuilder.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpecBuilder.kt @@ -18,16 +18,16 @@ package io.github.lcomment.korestdocs.spec -import io.github.lcomment.korestdocs.extension.putType -import io.github.lcomment.korestdocs.extension.toAttributes +import io.github.lcomment.korestdocs.extensions.putType +import io.github.lcomment.korestdocs.extensions.toAttributes import kotlin.reflect.KClass import org.springframework.restdocs.headers.HeaderDescriptor import org.springframework.restdocs.headers.HeaderDocumentation import org.springframework.restdocs.headers.RequestHeadersSnippet import org.springframework.restdocs.headers.ResponseHeadersSnippet -internal class HeadersSpecBuilder( - private val headers: MutableList = mutableListOf(), +class HeadersSpecBuilder( + private val headerDescriptors: MutableList = mutableListOf(), ) : HeadersSpec() { override fun add( @@ -37,6 +37,7 @@ internal class HeadersSpecBuilder( type: KClass, attributes: Map, ) { + headers.putIfAbsent(name, example.toString()) val descriptor = HeaderDocumentation.headerWithName(name) .description(description) .attributes(*attributes.putType(type).toAttributes()) @@ -45,12 +46,12 @@ internal class HeadersSpecBuilder( } override fun add(headerDescriptor: HeaderDescriptor) { - headers.add(headerDescriptor) + headerDescriptors.add(headerDescriptor) } fun buildRequestHeaders(attributes: Map): RequestHeadersSnippet = - HeaderDocumentation.requestHeaders(attributes, headers) + HeaderDocumentation.requestHeaders(attributes, headerDescriptors) fun buildResponseHeaders(attributes: Map): ResponseHeadersSnippet = - HeaderDocumentation.responseHeaders(attributes, headers) + HeaderDocumentation.responseHeaders(attributes, headerDescriptors) } diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/LinksSpec.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/LinksSpec.kt index 9f0c070..87789f8 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/LinksSpec.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/LinksSpec.kt @@ -22,7 +22,7 @@ import io.github.lcomment.korestdocs.annotation.RestdocsSpecDslMarker import org.springframework.restdocs.hypermedia.LinkDescriptor @RestdocsSpecDslMarker -interface LinksSpec { +interface LinksSpec : DocumentSpec { fun add( rel: String, diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/LinksSpecBuilder.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/LinksSpecBuilder.kt index b45761e..0bf5889 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/LinksSpecBuilder.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/LinksSpecBuilder.kt @@ -18,13 +18,13 @@ package io.github.lcomment.korestdocs.spec -import io.github.lcomment.korestdocs.extension.toAttributes +import io.github.lcomment.korestdocs.extensions.toAttributes import org.springframework.restdocs.hypermedia.HypermediaDocumentation import org.springframework.restdocs.hypermedia.LinkDescriptor import org.springframework.restdocs.hypermedia.LinkExtractor import org.springframework.restdocs.hypermedia.LinksSnippet -internal class LinksSpecBuilder( +class LinksSpecBuilder( private val links: MutableList = mutableListOf(), ) : LinksSpec { diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/ParametersSpec.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/ParametersSpec.kt index a2ca25e..30c205b 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/ParametersSpec.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/ParametersSpec.kt @@ -19,46 +19,6 @@ package io.github.lcomment.korestdocs.spec import io.github.lcomment.korestdocs.annotation.RestdocsSpecDslMarker -import kotlin.reflect.KClass -import org.springframework.restdocs.request.ParameterDescriptor @RestdocsSpecDslMarker -abstract class ParametersSpec { - - inline fun pathVariable( - name: String, - description: String? = null, - example: T, - attributes: Map = emptyMap(), - ) { - add(name, description, example, T::class, attributes) - } - - inline fun queryParameter( - name: String, - description: String? = null, - example: T, - attributes: Map = mapOf("optional" to false), - ) { - add(name, description, example, T::class, attributes) - } - - inline fun optionalQueryParameter( - name: String, - description: String? = null, - example: T, - attributes: Map = mapOf("optional" to true), - ) { - add(name, description, example, T::class, attributes) - } - - abstract fun add( - name: String, - description: String? = null, - example: T, - type: KClass, - attributes: Map = emptyMap(), - ) - - abstract fun add(parameterDescriptor: ParameterDescriptor) -} +interface ParametersSpec : DocumentSpec diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpec.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpec.kt new file mode 100644 index 0000000..16b01b8 --- /dev/null +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpec.kt @@ -0,0 +1,49 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.spec + +import io.github.lcomment.korestdocs.annotation.RestdocsSpecDslMarker +import kotlin.reflect.KClass +import org.springframework.restdocs.request.ParameterDescriptor + +@RestdocsSpecDslMarker +abstract class PathVariablesSpec : ParametersSpec { + + val pathVariables: MutableMap = mutableMapOf() + + inline fun pathVariable( + name: String, + description: String? = null, + example: T, + attributes: Map = emptyMap(), + ) { + pathVariables.putIfAbsent(name, example) + add(name, description, example, T::class, attributes) + } + + abstract fun add( + name: String, + description: String? = null, + example: T, + type: KClass, + attributes: Map = emptyMap(), + ) + + abstract fun add(parameterDescriptor: ParameterDescriptor) +} diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/ParametersSpecBuilder.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpecBuilder.kt similarity index 73% rename from korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/ParametersSpecBuilder.kt rename to korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpecBuilder.kt index f9eb445..6baedff 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/ParametersSpecBuilder.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpecBuilder.kt @@ -18,18 +18,17 @@ package io.github.lcomment.korestdocs.spec -import io.github.lcomment.korestdocs.extension.putFormat -import io.github.lcomment.korestdocs.extension.putType -import io.github.lcomment.korestdocs.extension.toAttributes +import io.github.lcomment.korestdocs.extensions.putFormat +import io.github.lcomment.korestdocs.extensions.putType +import io.github.lcomment.korestdocs.extensions.toAttributes import kotlin.reflect.KClass import org.springframework.restdocs.request.ParameterDescriptor import org.springframework.restdocs.request.PathParametersSnippet -import org.springframework.restdocs.request.QueryParametersSnippet import org.springframework.restdocs.request.RequestDocumentation -internal class ParametersSpecBuilder( +class PathVariablesSpecBuilder( private val parameters: MutableList = mutableListOf(), -) : ParametersSpec() { +) : PathVariablesSpec() { override fun add( name: String, @@ -52,17 +51,11 @@ internal class ParametersSpecBuilder( fun buildPathParameters( relaxed: Boolean, attributes: Map, - ): PathParametersSnippet = - if (relaxed) { + ): PathParametersSnippet { + return if (relaxed) { RequestDocumentation.relaxedPathParameters(attributes, parameters) } else { RequestDocumentation.pathParameters(attributes, parameters) } - - fun buildQueryParameters(relaxed: Boolean, attributes: Map): QueryParametersSnippet = - if (relaxed) { - RequestDocumentation.relaxedQueryParameters(attributes, parameters) - } else { - RequestDocumentation.queryParameters(attributes, parameters) - } + } } diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpec.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpec.kt new file mode 100644 index 0000000..2c7afdb --- /dev/null +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpec.kt @@ -0,0 +1,59 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.spec + +import io.github.lcomment.korestdocs.annotation.RestdocsSpecDslMarker +import kotlin.reflect.KClass +import org.springframework.restdocs.request.ParameterDescriptor + +@RestdocsSpecDslMarker +abstract class QueryParametersSpec : ParametersSpec { + + val queryParameters: MutableMap = mutableMapOf() + + inline fun queryParameter( + name: String, + description: String? = null, + example: T, + attributes: Map = mapOf("optional" to false), + ) { + queryParameters.putIfAbsent(name, example) + add(name, description, example, T::class, attributes) + } + + inline fun optionalQueryParameter( + name: String, + description: String? = null, + example: T, + attributes: Map = mapOf("optional" to true), + ) { + queryParameters.putIfAbsent(name, example) + add(name, description, example, T::class, attributes) + } + + abstract fun add( + name: String, + description: String? = null, + example: T, + type: KClass, + attributes: Map = emptyMap(), + ) + + abstract fun add(parameterDescriptor: ParameterDescriptor) +} diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpecBuilder.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpecBuilder.kt new file mode 100644 index 0000000..7e1bb5d --- /dev/null +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpecBuilder.kt @@ -0,0 +1,58 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.spec + +import io.github.lcomment.korestdocs.extensions.putFormat +import io.github.lcomment.korestdocs.extensions.putType +import io.github.lcomment.korestdocs.extensions.toAttributes +import kotlin.reflect.KClass +import org.springframework.restdocs.request.ParameterDescriptor +import org.springframework.restdocs.request.QueryParametersSnippet +import org.springframework.restdocs.request.RequestDocumentation + +class QueryParametersSpecBuilder( + private val parameters: MutableList = mutableListOf(), +) : QueryParametersSpec() { + + override fun add( + name: String, + description: String?, + example: T, + type: KClass, + attributes: Map, + ) { + val descriptor = RequestDocumentation.parameterWithName(name) + .description(description) + .attributes(*attributes.putFormat(type).putType(type).toAttributes()) + + add(descriptor) + } + + override fun add(parameterDescriptor: ParameterDescriptor) { + parameters.add(parameterDescriptor) + } + + fun buildQueryParameters(relaxed: Boolean, attributes: Map): QueryParametersSnippet { + return if (relaxed) { + RequestDocumentation.relaxedQueryParameters(attributes, parameters) + } else { + RequestDocumentation.queryParameters(attributes, parameters) + } + } +} diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpec.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpec.kt index 6d2dce8..c108552 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpec.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpec.kt @@ -19,18 +19,27 @@ package io.github.lcomment.korestdocs.spec import io.github.lcomment.korestdocs.annotation.RestdocsSpecDslMarker +import org.springframework.mock.web.MockMultipartFile import org.springframework.restdocs.request.RequestPartDescriptor @RestdocsSpecDslMarker -interface RequestPartsSpec { +abstract class RequestPartsSpec { - fun add( + val parts = mutableMapOf() + + abstract fun part( + name: String, + description: String? = null, + part: MockMultipartFile, + attributes: Map = mapOf("optional" to false), + ) + + abstract fun optionalPart( name: String, description: String? = null, - optional: Boolean = false, - ignored: Boolean = false, - attributes: Map = emptyMap(), + part: MockMultipartFile, + attributes: Map = mapOf("optional" to true), ) - fun add(requestPartDescriptor: RequestPartDescriptor) + abstract fun add(requestPartDescriptor: RequestPartDescriptor) } diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpecBuilder.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpecBuilder.kt index ef7fcd7..b0181ec 100644 --- a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpecBuilder.kt +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpecBuilder.kt @@ -18,33 +18,46 @@ package io.github.lcomment.korestdocs.spec -import io.github.lcomment.korestdocs.extension.toAttributes +import io.github.lcomment.korestdocs.extensions.toAttributes +import org.springframework.mock.web.MockMultipartFile import org.springframework.restdocs.request.RequestDocumentation import org.springframework.restdocs.request.RequestPartDescriptor import org.springframework.restdocs.request.RequestPartsSnippet -internal class RequestPartsSpecBuilder( - private val parts: MutableList = mutableListOf(), -) : RequestPartsSpec { +class RequestPartsSpecBuilder( + private val partDescriptors: MutableList = mutableListOf(), +) : RequestPartsSpec() { - override fun add( + override fun part( name: String, description: String?, - optional: Boolean, - ignored: Boolean, - attributes: Map, - ) = add( - RequestDocumentation.partWithName(name) + part: MockMultipartFile, + attributes: Map, + ) { + parts.putIfAbsent(name, part) + val descriptor = RequestDocumentation.partWithName(name) .description(description) - .apply { - if (optional) optional() - if (ignored) ignored() - } - .attributes(*attributes.toAttributes()), - ) + .attributes(*attributes.toAttributes()) + + add(descriptor) + } + + override fun optionalPart( + name: String, + description: String?, + part: MockMultipartFile, + attributes: Map, + ) { + parts.putIfAbsent(name, part) + val descriptor = RequestDocumentation.partWithName(name) + .description(description) + .attributes(*attributes.toAttributes()) + + add(descriptor) + } override fun add(requestPartDescriptor: RequestPartDescriptor) { - parts.add(requestPartDescriptor) + partDescriptors.add(requestPartDescriptor) } fun build( @@ -52,8 +65,8 @@ internal class RequestPartsSpecBuilder( attributes: Map, ): RequestPartsSnippet = if (relaxed) { - RequestDocumentation.relaxedRequestParts(attributes, parts) + RequestDocumentation.relaxedRequestParts(attributes, partDescriptors) } else { - RequestDocumentation.requestParts(attributes, parts) + RequestDocumentation.requestParts(attributes, partDescriptors) } } diff --git a/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/type/RequestType.kt b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/type/RequestType.kt new file mode 100644 index 0000000..d8f181a --- /dev/null +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/type/RequestType.kt @@ -0,0 +1,25 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.type + +enum class RequestType { + + HTTP, + MULTIPART, +} diff --git a/korest-docs-core/src/main/resources/org/springframework/restdocs/templates/request-part-fields.snippet b/korest-docs-core/src/main/resources/org/springframework/restdocs/templates/request-part-fields.snippet new file mode 100644 index 0000000..7ba32ce --- /dev/null +++ b/korest-docs-core/src/main/resources/org/springframework/restdocs/templates/request-part-fields.snippet @@ -0,0 +1,12 @@ +|=== +|Name|Type|Optional|Format|Description + +{{#fields}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{#optional}}O{{/optional}}{{^optional}}-{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{#format}}{{format}}{{/format}}{{^format}}-{{/format}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} +|=== diff --git a/korest-docs-core/src/main/resources/org/springframework/restdocs/templates/request-parts.snippet b/korest-docs-core/src/main/resources/org/springframework/restdocs/templates/request-parts.snippet new file mode 100644 index 0000000..5b2d0fe --- /dev/null +++ b/korest-docs-core/src/main/resources/org/springframework/restdocs/templates/request-parts.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Optional|Description + +{{#requestParts}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{#optional}}O{{/optional}}{{^optional}}-{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/requestParts}} +|=== diff --git a/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/extensions/KClassExtensionsTest.kt b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/extensions/KClassExtensionsTest.kt new file mode 100644 index 0000000..b4c6854 --- /dev/null +++ b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/extensions/KClassExtensionsTest.kt @@ -0,0 +1,205 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.extensions + +import io.github.lcomment.korestdocs.type.AnyField +import io.github.lcomment.korestdocs.type.ArrayField +import io.github.lcomment.korestdocs.type.BooleanField +import io.github.lcomment.korestdocs.type.DateField +import io.github.lcomment.korestdocs.type.DateTimeField +import io.github.lcomment.korestdocs.type.EnumField +import io.github.lcomment.korestdocs.type.NumberField +import io.github.lcomment.korestdocs.type.ObjectField +import io.github.lcomment.korestdocs.type.StringField +import io.github.lcomment.korestdocs.type.TimeField +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.ZonedDateTime +import java.util.Calendar +import java.util.Date + +internal class KClassExtensionsTest : DescribeSpec({ + + describe("toFieldType() Test") { + + context("if the class is String") { + val fieldType = String::class.toFieldType() + + it("should return StringField") { + fieldType shouldBe StringField + } + } + + context("if the class is Int") { + val fieldType = Int::class.toFieldType() + + it("should return NumberField") { + fieldType shouldBe NumberField + } + } + + context("if the class is Long") { + val fieldType = Long::class.toFieldType() + + it("should return NumberField") { + fieldType shouldBe NumberField + } + } + + context("if the class is Short") { + val fieldType = Short::class.toFieldType() + + it("should return NumberField") { + fieldType shouldBe NumberField + } + } + + context("if the class is Byte") { + val fieldType = Byte::class.toFieldType() + + it("should return NumberField") { + fieldType shouldBe NumberField + } + } + + context("if the class is Double") { + val fieldType = Double::class.toFieldType() + + it("should return NumberField") { + fieldType shouldBe NumberField + } + } + + context("if the class is Float") { + val fieldType = Float::class.toFieldType() + + it("should return NumberField") { + fieldType shouldBe NumberField + } + } + + context("if the class is Boolean") { + val fieldType = Boolean::class.toFieldType() + + it("should return BooleanField") { + fieldType shouldBe BooleanField + } + } + + context("if the class is List") { + val fieldType = List::class.toFieldType() + + it("should return ArrayField") { + fieldType shouldBe ArrayField + } + } + + context("if the class is Set") { + val fieldType = Set::class.toFieldType() + + it("should return ArrayField") { + fieldType shouldBe ArrayField + } + } + + context("if the class is Map") { + val fieldType = Map::class.toFieldType() + + it("should return ObjectField") { + fieldType shouldBe ObjectField + } + } + + context("if the class is Any") { + val fieldType = Any::class.toFieldType() + + it("should return AnyField") { + fieldType shouldBe AnyField + } + } + + context("if the class is Enum") { + val fieldType = ExampleEnum::class.toFieldType() + + it("should return EnumField") { + fieldType shouldBe EnumField + } + } + + context("if the class is LocalDate") { + val fieldType = LocalDate::class.toFieldType() + + it("should return DateField") { + fieldType shouldBe DateField + } + } + + context("if the class is LocalTime") { + val fieldType = LocalTime::class.toFieldType() + + it("should return DateField") { + fieldType shouldBe TimeField + } + } + + context("if the class is LocalDateTime") { + val fieldType = LocalDateTime::class.toFieldType() + + it("should return DateTimeField") { + fieldType shouldBe DateTimeField + } + } + + context("if the class is ZonedDateTime") { + val fieldType = ZonedDateTime::class.toFieldType() + + it("should return DateTimeField") { + fieldType shouldBe DateTimeField + } + } + + context("if the class is OffsetDateTime") { + val fieldType = OffsetDateTime::class.toFieldType() + + it("should return DateTimeField") { + fieldType shouldBe DateTimeField + } + } + + context("if the class is Date") { + val fieldType = Date::class.toFieldType() + + it("should return DateTimeField") { + fieldType shouldBe DateTimeField + } + } + + context("if the class is Calendar") { + val fieldType = Calendar::class.toFieldType() + + it("should return DateTimeField") { + fieldType shouldBe DateTimeField + } + } + } +}) diff --git a/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/extensions/MapExtensionsTest.kt b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/extensions/MapExtensionsTest.kt new file mode 100644 index 0000000..7e540c1 --- /dev/null +++ b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/extensions/MapExtensionsTest.kt @@ -0,0 +1,116 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.extensions + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime + +internal class MapExtensionsTest : DescribeSpec({ + + describe("toAttributes() Method Test") { + + context("if the map is not empty") { + val attributeMap = mapOf("optional" to true, "ignored" to false) + + it("should return an array of attributes") { + val attributes = attributeMap.toAttributes() + + attributes.size shouldBe 2 + attributes[0].key shouldBe "optional" + attributes[0].value shouldBe true + attributes[1].key shouldBe "ignored" + attributes[1].value shouldBe false + } + } + + context("if the map is empty") { + val attributeMap = mapOf() + + it("should return an empty array") { + val attributes = attributeMap.toAttributes() + + attributes.size shouldBe 0 + } + } + } + + describe("putFormat() Method Test") { + + context("if the type is EnumField") { + val attributeMap = emptyMap() + + it("should return the map with the format key") { + val formattedMap = attributeMap.putFormat(ExampleEnum::class) + println(formattedMap) + formattedMap.size shouldBe 1 + formattedMap["format"] shouldBe listOf("A", "B", "C").joinToString() + } + } + + context("if the type is DateTimeField") { + val attributeMap = emptyMap() + + it("should return the map with the format key") { + val formattedMap = attributeMap.putFormat(LocalDateTime::class) + + formattedMap.size shouldBe 1 + formattedMap["format"] shouldBe "yyyy-mm-ddThh:mm:ss" + } + } + + context("if the type is DateField") { + val attributeMap = emptyMap() + + it("should return the map with the format key") { + val formattedMap = attributeMap.putFormat(LocalDate::class) + + formattedMap.size shouldBe 1 + formattedMap["format"] shouldBe "yyyy-mm-dd" + } + } + + context("if the type is TimeField") { + val attributeMap = emptyMap() + + it("should return the map with the format key") { + val formattedMap = attributeMap.putFormat(LocalTime::class) + + formattedMap.size shouldBe 1 + formattedMap["format"] shouldBe "hh:mm:ss" + } + } + + context("if the type is not EnumField, DateTimeField, DateField, or TimeField") { + val attributeMap = emptyMap() + + it("should return the map without the format key") { + val formattedMap = attributeMap.putFormat(String::class) + + formattedMap.size shouldBe 0 + } + } + } +}) + +internal enum class ExampleEnum { + A, B, C, +} diff --git a/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpecTest.kt b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpecTest.kt new file mode 100644 index 0000000..5552d39 --- /dev/null +++ b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpecTest.kt @@ -0,0 +1,100 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.spec + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +internal class FieldsSpecTest : DescribeSpec({ + + describe("field() Method Test") { + val fieldsSpec = FieldsSpecBuilder() + + context("if add field") { + val example = "test" + + it("fieldsSpec should have one field") { + fieldsSpec.field( + path = "test", + description = "test", + example = example, + ) + + fieldsSpec.fields.size shouldBe 1 + fieldsSpec.fields["test"] shouldBe example + } + } + } + + describe("optionalField() Method Test") { + val fieldsSpec = FieldsSpecBuilder() + + context("if add field") { + val example = "test" + + it("fieldsSpec should have one field") { + fieldsSpec.optionalField( + path = "test", + description = "test", + example = example, + ) + + fieldsSpec.fields.size shouldBe 1 + fieldsSpec.fields["test"] shouldBe example + } + } + } + + describe("ignoredField() Method Test") { + val fieldsSpec = FieldsSpecBuilder() + + context("if add field") { + val example = "test" + + it("fieldsSpec should have one field") { + fieldsSpec.ignoredField( + path = "test", + description = "test", + example = example, + ) + + fieldsSpec.fields.size shouldBe 1 + fieldsSpec.fields["test"] shouldBe example + } + } + } + + describe("subsectionField() Method Test") { + val fieldsSpec = FieldsSpecBuilder() + + context("if add field") { + val example = "test" + + it("fieldsSpec should have one field") { + fieldsSpec.subsectionField( + path = "test", + description = "test", + example = example, + ) + + fieldsSpec.fields.size shouldBe 0 + } + } + } +}) diff --git a/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpecTest.kt b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpecTest.kt new file mode 100644 index 0000000..c0befe0 --- /dev/null +++ b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpecTest.kt @@ -0,0 +1,63 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.spec + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +internal class HeadersSpecTest : DescribeSpec({ + + describe("header() Method Test") { + val headersSpec = HeadersSpecBuilder() + + context("if add header") { + val example = "test" + + it("fieldsSpec should have one field") { + headersSpec.header( + name = "test", + description = "test", + example = example, + ) + + headersSpec.headers.size shouldBe 1 + headersSpec.headers["test"] shouldBe example + } + } + } + + describe("optionalHeader() Method Test") { + val headersSpec = HeadersSpecBuilder() + + context("if add header") { + val example = "test" + + it("headersSpec should have one header") { + headersSpec.header( + name = "test", + description = "test", + example = example, + ) + + headersSpec.headers.size shouldBe 1 + headersSpec.headers["test"] shouldBe example + } + } + } +}) diff --git a/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/LinksSpecTest.kt b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/LinksSpecTest.kt new file mode 100644 index 0000000..5460012 --- /dev/null +++ b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/LinksSpecTest.kt @@ -0,0 +1,41 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.spec + +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.core.spec.style.DescribeSpec + +internal class LinksSpecTest : DescribeSpec({ + + describe("add() Method Test") { + val linksSpec = LinksSpecBuilder() + + context("if add link") { + + it("should successfully add link") { + shouldNotThrow { + linksSpec.add( + rel = "test", + description = "test", + ) + } + } + } + } +}) diff --git a/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpecTest.kt b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpecTest.kt new file mode 100644 index 0000000..56ada0b --- /dev/null +++ b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpecTest.kt @@ -0,0 +1,44 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.spec + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +internal class PathVariablesSpecTest : DescribeSpec({ + + describe("pathVariable() Method Test") { + val pathVariablesSpec = PathVariablesSpecBuilder() + + context("if add path variable") { + val example = "test" + + it("pathVariablesSpec should have one path variable") { + pathVariablesSpec.pathVariable( + name = "test", + description = "test", + example = example, + ) + + pathVariablesSpec.pathVariables.size shouldBe 1 + pathVariablesSpec.pathVariables["test"] shouldBe example + } + } + } +}) diff --git a/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpecTest.kt b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpecTest.kt new file mode 100644 index 0000000..834ca6b --- /dev/null +++ b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpecTest.kt @@ -0,0 +1,63 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.spec + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +internal class QueryParametersSpecTest : DescribeSpec({ + + describe("queryParameter() Method Test") { + val queryParametersSpec = QueryParametersSpecBuilder() + + context("if add query parameter") { + val example = "test" + + it("queryParametersSpec should have one query parameter") { + queryParametersSpec.queryParameter( + name = "test", + description = "test", + example = example, + ) + + queryParametersSpec.queryParameters.size shouldBe 1 + queryParametersSpec.queryParameters["test"] shouldBe example + } + } + } + + describe("optionalQueryParameter() Method Test") { + val queryParametersSpec = QueryParametersSpecBuilder() + + context("if add query parameter") { + val example = "test" + + it("queryParametersSpec should have one query parameter") { + queryParametersSpec.optionalQueryParameter( + name = "test", + description = "test", + example = example, + ) + + queryParametersSpec.queryParameters.size shouldBe 1 + queryParametersSpec.queryParameters["test"] shouldBe example + } + } + } +}) diff --git a/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpecTest.kt b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpecTest.kt new file mode 100644 index 0000000..4b424e9 --- /dev/null +++ b/korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpecTest.kt @@ -0,0 +1,64 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.spec + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import org.springframework.mock.web.MockMultipartFile + +class RequestPartsSpecTest : DescribeSpec({ + + describe("requestPart() Method Test") { + val requestPartsSpec = RequestPartsSpecBuilder() + + context("if add request part") { + val example = MockMultipartFile("image", "", "image/png", "image".toByteArray()) + + it("requestPartsSpec should have one request part") { + requestPartsSpec.part( + name = "test", + description = "test", + part = example, + ) + + requestPartsSpec.parts.size shouldBe 1 + requestPartsSpec.parts["test"] shouldBe example + } + } + } + + describe("optionalRequestPart() Method Test") { + val requestPartsSpec = RequestPartsSpecBuilder() + + context("if add request part") { + val example = MockMultipartFile("image", "", "image/png", "image".toByteArray()) + + it("requestPartsSpec should have one request part") { + requestPartsSpec.optionalPart( + name = "test", + description = "test", + part = example, + ) + + requestPartsSpec.parts.size shouldBe 1 + requestPartsSpec.parts["test"] shouldBe example + } + } + } +}) diff --git a/korest-docs-mockmvc/build.gradle.kts b/korest-docs-mockmvc/build.gradle.kts index 9ca0977..60ce128 100644 --- a/korest-docs-mockmvc/build.gradle.kts +++ b/korest-docs-mockmvc/build.gradle.kts @@ -7,6 +7,15 @@ plugins { dependencies { api(projects.core) api(libs.restdocs.mockmvc) + implementation(libs.jakarta.servlet.api) + implementation(libs.spring.context) + implementation(libs.junit.jupiter) + implementation(libs.jackson.databind) + implementation(libs.jackson.datatype.jsr310) + implementation(libs.jackson.core) + + testImplementation(libs.kotest.junit) + testImplementation(libs.kotest.assertions.core) } tasks.bootJar { diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/KorestDocumentationExtension.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/KorestDocumentationExtension.kt new file mode 100644 index 0000000..d4899fd --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/KorestDocumentationExtension.kt @@ -0,0 +1,85 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc + +import io.github.lcomment.korestdocs.mockmvc.context.DefaultMockMvcContext +import io.github.lcomment.korestdocs.mockmvc.context.MockMvcContextHolder +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.ParameterContext +import org.junit.jupiter.api.extension.ParameterResolver +import org.springframework.restdocs.ManualRestDocumentation +import org.springframework.restdocs.RestDocumentationContextProvider +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.web.context.WebApplicationContext + +class KorestDocumentationExtension( + private val outputDirectory: String? = null, +) : BeforeEachCallback, AfterEachCallback, ParameterResolver { + + override fun beforeEach(context: ExtensionContext) { + val delegate = getDelegate(context) + delegate.beforeTest(context.requiredTestClass, context.requiredTestMethod.name) + + val applicationContext = SpringExtension.getApplicationContext(context) as WebApplicationContext + MockMvcContextHolder.setContext(DefaultMockMvcContext(applicationContext, delegate)) + } + + override fun afterEach(context: ExtensionContext) { + this.getDelegate(context).afterTest() + MockMvcContextHolder.clearContext() + } + + override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean { + if (isTestMethodContext(extensionContext)) { + return RestDocumentationContextProvider::class.java.isAssignableFrom(parameterContext.parameter.getType()) + } + + return false + } + + override fun resolveParameter(parameterContext: ParameterContext, context: ExtensionContext): Any { + return RestDocumentationContextProvider { + getDelegate(context).beforeOperation() + } + } + + private fun isTestMethodContext(context: ExtensionContext): Boolean { + return context.testClass.isPresent && context.testMethod.isPresent + } + + private fun getDelegate(context: ExtensionContext): ManualRestDocumentation { + val namespace = ExtensionContext.Namespace.create(javaClass, context.uniqueId) + return context.getStore(namespace) + .getOrComputeIfAbsent( + ManualRestDocumentation::class.java, + this::createManualRestDocumentation, + ManualRestDocumentation::class.java, + ) + } + + private fun createManualRestDocumentation(key: Class): ManualRestDocumentation { + return if (this.outputDirectory != null) { + ManualRestDocumentation(this.outputDirectory) + } else { + ManualRestDocumentation() + } + } +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGenerator.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGenerator.kt new file mode 100644 index 0000000..8a77fe5 --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGenerator.kt @@ -0,0 +1,124 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc + +import io.github.lcomment.korestdocs.spec.FieldsSpec +import io.github.lcomment.korestdocs.spec.HeadersSpec +import io.github.lcomment.korestdocs.spec.PathVariablesSpec +import io.github.lcomment.korestdocs.spec.QueryParametersSpec +import io.github.lcomment.korestdocs.spec.RequestPartsSpec +import io.github.lcomment.korestdocs.type.RequestType +import org.springframework.http.HttpMethod +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor +import org.springframework.restdocs.payload.PayloadSubsectionExtractor +import org.springframework.restdocs.snippet.Snippet + +interface MockMvcDocumentGenerator { + + val identifier: String + + val requestPreprocessor: OperationRequestPreprocessor + + val responsePreprocessor: OperationResponsePreprocessor + + val snippets: List + + var requestType: RequestType? + + var method: HttpMethod? + + var urlTemplate: String? + + var headersSpec: HeadersSpec? + + var pathVariablesSpec: PathVariablesSpec? + + var queryParametersSpec: QueryParametersSpec? + + var requestFieldsSpec: FieldsSpec? + + var requestPartsSpec: RequestPartsSpec? + + var requestPartFieldsSpec: Map? + + fun addSnippet(snippet: Snippet) + + fun request( + method: HttpMethod, + urlTemplate: String, + configure: PathVariablesSpec.() -> Unit = {}, + ) + + fun multipart( + method: HttpMethod, + urlTemplate: String, + configure: PathVariablesSpec.() -> Unit = {}, + ) + + fun requestHeader( + attributes: Map = emptyMap(), + configure: HeadersSpec.() -> Unit, + ) + + fun pathParameter( + relaxed: Boolean = false, + attributes: Map = emptyMap(), + configure: PathVariablesSpec.() -> Unit, + ) + + fun requestParameter( + relaxed: Boolean = false, + attributes: Map = emptyMap(), + configure: QueryParametersSpec.() -> Unit, + ) + + fun requestField( + relaxed: Boolean = false, + subsectionExtractor: PayloadSubsectionExtractor<*>? = null, + attributes: Map = emptyMap(), + configure: FieldsSpec.() -> Unit, + ) + + fun requestPart( + relaxed: Boolean = false, + attributes: Map = emptyMap(), + configure: RequestPartsSpec.() -> Unit, + ) + + fun requestPartField( + part: String, + relaxed: Boolean = false, + subsectionExtractor: PayloadSubsectionExtractor<*>? = null, + attributes: Map = emptyMap(), + configure: FieldsSpec.() -> Unit, + ) + + fun responseHeader( + attributes: Map = emptyMap(), + configure: HeadersSpec.() -> Unit, + ) + + fun responseField( + relaxed: Boolean = false, + subsectionExtractor: PayloadSubsectionExtractor<*>? = null, + attributes: Map = emptyMap(), + configure: FieldsSpec.() -> Unit, + ) +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGeneratorBuilder.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGeneratorBuilder.kt new file mode 100644 index 0000000..f713ad1 --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGeneratorBuilder.kt @@ -0,0 +1,177 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc + +import io.github.lcomment.korestdocs.spec.FieldsSpec +import io.github.lcomment.korestdocs.spec.FieldsSpecBuilder +import io.github.lcomment.korestdocs.spec.HeadersSpec +import io.github.lcomment.korestdocs.spec.HeadersSpecBuilder +import io.github.lcomment.korestdocs.spec.PathVariablesSpec +import io.github.lcomment.korestdocs.spec.PathVariablesSpecBuilder +import io.github.lcomment.korestdocs.spec.QueryParametersSpec +import io.github.lcomment.korestdocs.spec.QueryParametersSpecBuilder +import io.github.lcomment.korestdocs.spec.RequestPartsSpec +import io.github.lcomment.korestdocs.spec.RequestPartsSpecBuilder +import io.github.lcomment.korestdocs.type.RequestType +import org.springframework.http.HttpMethod +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor +import org.springframework.restdocs.payload.PayloadSubsectionExtractor +import org.springframework.restdocs.snippet.Snippet + +internal class MockMvcDocumentGeneratorBuilder( + override val identifier: String, + override var method: HttpMethod? = null, + override var urlTemplate: String? = null, + override var requestType: RequestType? = null, + override var headersSpec: HeadersSpec? = null, + override var pathVariablesSpec: PathVariablesSpec? = null, + override var queryParametersSpec: QueryParametersSpec? = null, + override var requestFieldsSpec: FieldsSpec? = null, + override var requestPartsSpec: RequestPartsSpec? = null, + override var requestPartFieldsSpec: Map? = null, + override val requestPreprocessor: OperationRequestPreprocessor = OperationRequestPreprocessor { r -> r }, + override val responsePreprocessor: OperationResponsePreprocessor = OperationResponsePreprocessor { r -> r }, + override val snippets: MutableList = mutableListOf(), +) : MockMvcDocumentGenerator { + + override fun addSnippet(snippet: Snippet) { + snippets.add(snippet) + } + + override fun request( + method: HttpMethod, + urlTemplate: String, + configure: PathVariablesSpec.() -> Unit, + ) { + this.method = method + this.urlTemplate = urlTemplate + this.requestType = RequestType.HTTP + + val specBuilder = PathVariablesSpecBuilder().apply(configure) + this.pathVariablesSpec = specBuilder + addSnippet(specBuilder.buildPathParameters(false, emptyMap())) + } + + override fun multipart( + method: HttpMethod, + urlTemplate: String, + configure: PathVariablesSpec.() -> Unit, + ) { + this.method = method + this.urlTemplate = urlTemplate + this.requestType = RequestType.MULTIPART + + val specBuilder = PathVariablesSpecBuilder().apply(configure) + this.pathVariablesSpec = specBuilder + addSnippet(specBuilder.buildPathParameters(false, emptyMap())) + } + + override fun requestHeader( + attributes: Map, + configure: HeadersSpec.() -> Unit, + ) { + val spec = HeadersSpecBuilder().apply(configure) + + this.headersSpec = spec + addSnippet(spec.buildRequestHeaders(attributes)) + } + + override fun pathParameter( + relaxed: Boolean, + attributes: Map, + configure: PathVariablesSpec.() -> Unit, + ) { + val spec = PathVariablesSpecBuilder().apply(configure) + this.pathVariablesSpec = spec + + addSnippet(spec.buildPathParameters(relaxed, attributes)) + } + + override fun requestParameter( + relaxed: Boolean, + attributes: Map, + configure: QueryParametersSpec.() -> Unit, + ) { + val spec = QueryParametersSpecBuilder().apply(configure) + this.queryParametersSpec = spec + + addSnippet(spec.buildQueryParameters(relaxed, attributes)) + } + + override fun requestField( + relaxed: Boolean, + subsectionExtractor: PayloadSubsectionExtractor<*>?, + attributes: Map, + configure: FieldsSpec.() -> Unit, + ) { + val spec = FieldsSpecBuilder().apply(configure) + this.requestFieldsSpec = spec + + addSnippet(spec.buildRequestFields(relaxed, subsectionExtractor, attributes)) + } + + override fun requestPart( + relaxed: Boolean, + attributes: Map, + configure: RequestPartsSpec.() -> Unit, + ) { + val spec = RequestPartsSpecBuilder().apply(configure) + this.requestPartsSpec = spec + + addSnippet(spec.build(relaxed, attributes)) + } + + override fun requestPartField( + part: String, + relaxed: Boolean, + subsectionExtractor: PayloadSubsectionExtractor<*>?, + attributes: Map, + configure: FieldsSpec.() -> Unit, + ) { + val spec = FieldsSpecBuilder().apply(configure) + this.requestPartFieldsSpec = mapOf(part to spec) + + addSnippet(spec.buildRequestPartFields(part, relaxed, subsectionExtractor, attributes)) + } + + override fun responseHeader( + attributes: Map, + configure: HeadersSpec.() -> Unit, + ) { + val snippet = HeadersSpecBuilder() + .apply(configure) + .buildResponseHeaders(attributes) + + addSnippet(snippet) + } + + override fun responseField( + relaxed: Boolean, + subsectionExtractor: PayloadSubsectionExtractor<*>?, + attributes: Map, + configure: FieldsSpec.() -> Unit, + ) { + val snippet = FieldsSpecBuilder() + .apply(configure) + .buildResponseFields(relaxed, subsectionExtractor, attributes) + + addSnippet(snippet) + } +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentation.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentation.kt new file mode 100644 index 0000000..a1770ca --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentation.kt @@ -0,0 +1,150 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc + +import io.github.lcomment.korestdocs.mockmvc.context.MockMvcContextHolder +import io.github.lcomment.korestdocs.mockmvc.extensions.documentationScope +import io.github.lcomment.korestdocs.mockmvc.extensions.extractPathParameters +import io.github.lcomment.korestdocs.mockmvc.extensions.multipartWithDocs +import io.github.lcomment.korestdocs.mockmvc.extensions.requestWithDocs +import io.github.lcomment.korestdocs.mockmvc.extensions.toMockMultipartFile +import io.github.lcomment.korestdocs.mockmvc.extensions.toResultHandler +import io.github.lcomment.korestdocs.spec.FieldsSpec +import io.github.lcomment.korestdocs.spec.HeadersSpec +import io.github.lcomment.korestdocs.spec.QueryParametersSpec +import io.github.lcomment.korestdocs.spec.RequestPartsSpec +import io.github.lcomment.korestdocs.type.RequestType +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.ResultActionsDsl +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule + +fun documentation( + identifier: String, + configure: MockMvcDocumentGenerator.() -> Unit, +): ResultActionsDsl { + val documentSpec = documentationScope(identifier).apply(configure) + val method = documentSpec.method ?: throw IllegalArgumentException("Not Exist method") + val urlTemplate = documentSpec.urlTemplate ?: throw IllegalArgumentException("Not Exist url template") + val requestType = documentSpec.requestType ?: throw IllegalArgumentException("Not Exist request type") + + val mockMvc = MockMvcContextHolder.getContext().getMockMvc() + val headersSpec = documentSpec.headersSpec + val pathVariables = extractPathParameters(urlTemplate, documentSpec.pathVariablesSpec?.pathVariables) + val queryParametersSpec = documentSpec.queryParametersSpec + val requestFieldsSpec = documentSpec.requestFieldsSpec + val requestPartsSpec = documentSpec.requestPartsSpec + val requestPartFieldsSpec = documentSpec.requestPartFieldsSpec + + val resultActionsDsl = when (requestType) { + RequestType.HTTP -> requestHttp( + urlTemplate = urlTemplate, + pathVariables = pathVariables, + method = method, + mockMvc = mockMvc, + headersSpec = headersSpec, + queryParameterSpec = queryParametersSpec, + requestFieldsSpec = requestFieldsSpec, + ) + + RequestType.MULTIPART -> requestMultipart( + urlTemplate = urlTemplate, + pathVariables = pathVariables, + method = method, + mockMvc = mockMvc, + headersSpec = headersSpec, + queryParametersSpec = queryParametersSpec, + requestPartFieldsSpec = requestPartFieldsSpec, + requestPartsSpec = requestPartsSpec, + ) + } + + return resultActionsDsl.andDo { handle(documentSpec.toResultHandler()) } +} + +private fun requestHttp( + urlTemplate: String, + pathVariables: List, + method: HttpMethod, + mockMvc: MockMvc, + headersSpec: HeadersSpec?, + queryParameterSpec: QueryParametersSpec?, + requestFieldsSpec: FieldsSpec?, +): ResultActionsDsl { + return mockMvc.requestWithDocs(method, urlTemplate, *pathVariables.toTypedArray()) { + queryParameterSpec?.queryParameters?.forEach { (key, value) -> + param(key, value.toString()) + } + + headersSpec?.headers?.forEach { (key, value) -> + header(key, value.toString()) + } + + content = toJson(requestFieldsSpec?.fields ?: emptyMap()) + contentType = MediaType.APPLICATION_JSON + } +} + +private fun requestMultipart( + urlTemplate: String, + pathVariables: List, + method: HttpMethod, + mockMvc: MockMvc, + headersSpec: HeadersSpec?, + queryParametersSpec: QueryParametersSpec?, + requestPartFieldsSpec: Map?, + requestPartsSpec: RequestPartsSpec?, +): ResultActionsDsl { + return mockMvc.multipartWithDocs(method, urlTemplate, *pathVariables.toTypedArray()) { + queryParametersSpec?.queryParameters?.forEach { (key, value) -> + param(key, value.toString()) + } + + headersSpec?.headers?.forEach { (key, value) -> + header(key, value.toString()) + } + + requestPartsSpec?.parts?.values?.forEach { + file(it) + } + + requestPartFieldsSpec?.forEach { (key, value) -> + file(value.fields.toMockMultipartFile(key)) + } + + contentType = MediaType.MULTIPART_FORM_DATA + accept = MediaType.APPLICATION_JSON + + with { + it.method = method.name() + it + } + } +} + +fun toJson(value: Any): String { + return mapper.writeValueAsString(value) +} + +private val mapper: ObjectMapper = ObjectMapper() + .registerModule(JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/DefaultMockMvcContext.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/DefaultMockMvcContext.kt new file mode 100644 index 0000000..df79abe --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/DefaultMockMvcContext.kt @@ -0,0 +1,45 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc.context + +import org.springframework.restdocs.RestDocumentationContextProvider +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.context.WebApplicationContext + +internal class DefaultMockMvcContext( + private val mockMvc: MockMvc, +) : MockMvcContext { + + constructor( + applicationContext: WebApplicationContext, + restDocumentationContextProvider: RestDocumentationContextProvider, + ) : this( + MockMvcBuilders + .webAppContextSetup(applicationContext) + .apply(documentationConfiguration(restDocumentationContextProvider)) + .build(), + ) + + override fun getMockMvc(): MockMvc { + return mockMvc + } +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContext.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContext.kt new file mode 100644 index 0000000..501cd33 --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContext.kt @@ -0,0 +1,27 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc.context + +import java.io.Serializable +import org.springframework.test.web.servlet.MockMvc + +interface MockMvcContext : Serializable { + + fun getMockMvc(): MockMvc +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContextHolder.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContextHolder.kt new file mode 100644 index 0000000..709b24f --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContextHolder.kt @@ -0,0 +1,42 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc.context + +object MockMvcContextHolder { + + private lateinit var strategy: MockMvcContextHolderStrategy + + fun clearContext() { + strategy.clearContext() + } + + fun getContext(): MockMvcContext = strategy.getContext() + + fun setContext(context: MockMvcContext) { + strategy.setContext(context) + } + + private fun initialize() { + strategy = ThreadLocalMockMvcContextHolderStrategy() + } + + init { + initialize() + } +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContextHolderStrategy.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContextHolderStrategy.kt new file mode 100644 index 0000000..d65e6ea --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContextHolderStrategy.kt @@ -0,0 +1,28 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc.context + +interface MockMvcContextHolderStrategy { + + fun clearContext() + + fun getContext(): MockMvcContext + + fun setContext(context: MockMvcContext) +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/ThreadLocalMockMvcContextHolderStrategy.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/ThreadLocalMockMvcContextHolderStrategy.kt new file mode 100644 index 0000000..063cac6 --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/ThreadLocalMockMvcContextHolderStrategy.kt @@ -0,0 +1,45 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc.context + +internal class ThreadLocalMockMvcContextHolderStrategy : MockMvcContextHolderStrategy { + + override fun clearContext() { + CONTEXT_HOLDER.remove() + } + + override fun getContext(): MockMvcContext { + var ctx = CONTEXT_HOLDER.get() + + if (ctx == null) { + throw IllegalStateException("Not Exist MockMvcContext") + } + + return ctx + } + + override fun setContext(context: MockMvcContext) { + CONTEXT_HOLDER.set(context) + } + + companion object { + + private val CONTEXT_HOLDER: ThreadLocal = ThreadLocal() + } +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MapExtensions.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MapExtensions.kt new file mode 100644 index 0000000..c9ebd3d --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MapExtensions.kt @@ -0,0 +1,28 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc.extensions + +import io.github.lcomment.korestdocs.mockmvc.toJson +import org.springframework.mock.web.MockMultipartFile + +fun MutableMap.toMockMultipartFile( + name: String, +): MockMultipartFile { + return MockMultipartFile(name, "", "application/json", toJson(this).toByteArray()) +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MockMvcDocumentGeneratorExtensions.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MockMvcDocumentGeneratorExtensions.kt new file mode 100644 index 0000000..a7ff031 --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MockMvcDocumentGeneratorExtensions.kt @@ -0,0 +1,8 @@ +package io.github.lcomment.korestdocs.mockmvc.extensions + +import io.github.lcomment.korestdocs.mockmvc.MockMvcDocumentGenerator +import io.github.lcomment.korestdocs.mockmvc.MockMvcDocumentGeneratorBuilder + +fun documentationScope(identifier: String): MockMvcDocumentGenerator { + return MockMvcDocumentGeneratorBuilder(identifier) +} diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcExtension.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MockMvcExtensions.kt similarity index 99% rename from korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcExtension.kt rename to korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MockMvcExtensions.kt index 7c83763..5d1e459 100644 --- a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcExtension.kt +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MockMvcExtensions.kt @@ -16,7 +16,7 @@ * limitations under the License. */ -package io.github.lcomment.korestdocs.mockmvc +package io.github.lcomment.korestdocs.mockmvc.extensions import java.net.URI import org.springframework.http.HttpMethod diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/ResultActionsExtension.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/ResultActionsExtensions.kt similarity index 82% rename from korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/ResultActionsExtension.kt rename to korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/ResultActionsExtensions.kt index ef65709..2902cf4 100644 --- a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/ResultActionsExtension.kt +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/ResultActionsExtensions.kt @@ -16,10 +16,9 @@ * limitations under the License. */ -package io.github.lcomment.korestdocs.mockmvc +package io.github.lcomment.korestdocs.mockmvc.extensions -import io.github.lcomment.korestdocs.spec.DocumentSpec -import io.github.lcomment.korestdocs.spec.documentationScope +import io.github.lcomment.korestdocs.mockmvc.MockMvcDocumentGenerator import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation import org.springframework.test.web.servlet.ResultActions import org.springframework.test.web.servlet.ResultActionsDsl @@ -27,7 +26,7 @@ import org.springframework.test.web.servlet.ResultHandler fun ResultActions.andDocument( identifier: String, - configure: DocumentSpec.() -> Unit, + configure: MockMvcDocumentGenerator.() -> Unit, ): ResultActions { val documentSpec = documentationScope(identifier).apply(configure) @@ -36,14 +35,14 @@ fun ResultActions.andDocument( fun ResultActionsDsl.andDocument( identifier: String, - configure: DocumentSpec.() -> Unit, + configure: MockMvcDocumentGenerator.() -> Unit, ): ResultActionsDsl { val documentSpec = documentationScope(identifier).apply(configure) return andDo { handle(documentSpec.toResultHandler()) } } -private fun DocumentSpec.toResultHandler(): ResultHandler { +internal fun MockMvcDocumentGenerator.toResultHandler(): ResultHandler { return MockMvcRestDocumentation.document( identifier, requestPreprocessor, diff --git a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/StringExtensions.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/StringExtensions.kt new file mode 100644 index 0000000..722ea43 --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/StringExtensions.kt @@ -0,0 +1,34 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc.extensions + +fun extractPathParameters( + urlTemplate: String, + pathVariables: Map?, +): List { + if (pathVariables.isNullOrEmpty()) { + return emptyList() + } + + val regex = "\\{(.*?)}".toRegex() + + return regex.findAll(urlTemplate) + .mapNotNull { pathVariables[it.groupValues[1]] } + .toList() +} diff --git a/korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGeneratorTest.kt b/korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGeneratorTest.kt new file mode 100644 index 0000000..2255f62 --- /dev/null +++ b/korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGeneratorTest.kt @@ -0,0 +1,130 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc + +import io.github.lcomment.korestdocs.type.RequestType +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.springframework.http.HttpMethod + +internal class MockMvcDocumentGeneratorTest : DescribeSpec({ + + describe("request() and multipart()") { + val generator = MockMvcDocumentGeneratorBuilder(identifier = "test-doc") + + context("if call request()") { + generator.request(HttpMethod.GET, "/api/example/{id}") { + pathVariable("id", "id", "id") + } + + it("set method and urlTemplate") { + generator.method shouldBe HttpMethod.GET + generator.urlTemplate shouldBe "/api/example/{id}" + } + + it("set RequestType to HTTP") { + generator.requestType shouldBe RequestType.HTTP + } + + it("add PathVariablesSpec") { + generator.pathVariablesSpec shouldNotBe null + } + } + + context("if call multipart()") { + generator.multipart(HttpMethod.POST, "/api/example/{id}") { + pathVariable("id", "id", "id") + } + + it("set method and urlTemplate") { + generator.method shouldBe HttpMethod.POST + generator.urlTemplate shouldBe "/api/example/{id}" + } + + it("set RequestType to MULTIPART") { + generator.requestType shouldBe RequestType.MULTIPART + } + + it("addPathVariablesSpec") { + generator.pathVariablesSpec shouldNotBe null + } + } + } + + describe("requestHeader() Test") { + val generator = MockMvcDocumentGeneratorBuilder(identifier = "test-doc") + + context("if add headers") { + generator.requestHeader { + header("Authorization", "Bearer token", "Bearer abcdefghijklmnopqrstuvwxyz") + header("X-Test-Header", "TestHeader", "TestHeader") + } + + it("set headersSpec") { + generator.headersSpec shouldNotBe null + } + } + } + + describe("pathParameter() Test") { + val generator = MockMvcDocumentGeneratorBuilder(identifier = "test-doc") + + context("if add parameters") { + generator.pathParameter { + pathVariable("page", "page", "1") + pathVariable("size", "size", "10") + } + + it("set parametersSpec") { + generator.pathVariablesSpec shouldNotBe null + } + } + } + + describe("queryParameter() Test") { + val generator = MockMvcDocumentGeneratorBuilder(identifier = "test-doc") + + context("if add parameters") { + generator.requestParameter { + queryParameter("page", "page", "1") + queryParameter("size", "size", "10") + } + + it("set parametersSpec") { + generator.queryParametersSpec shouldNotBe null + } + } + } + + describe("requestField() Test") { + val generator = MockMvcDocumentGeneratorBuilder(identifier = "test-doc") + + context("if add response fields") { + generator.requestField { + field("id", "id", "id") + field("name", "name", "name") + } + + it("set responseFieldsSpec") { + generator.requestFieldsSpec shouldNotBe null + } + } + } +}) diff --git a/korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MapExtensionsTest.kt b/korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MapExtensionsTest.kt new file mode 100644 index 0000000..0da1350 --- /dev/null +++ b/korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MapExtensionsTest.kt @@ -0,0 +1,46 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc.extensions + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +internal class MapExtensionsTest : DescribeSpec({ + + describe("toMockMultipartFile() Test") { + + context("if the map is empty") { + val map = mutableMapOf() + val mockMultipartFile = map.toMockMultipartFile("file") + + it("should return MockMultipartFile") { + mockMultipartFile.name shouldBe "file" + } + } + + context("if the map has one entry") { + val map = mutableMapOf("key" to "value") + val mockMultipartFile = map.toMockMultipartFile("file") + + it("should return MockMultipartFile") { + mockMultipartFile.name shouldBe "file" + } + } + } +}) diff --git a/korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/StringExtensionsTest.kt b/korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/StringExtensionsTest.kt new file mode 100644 index 0000000..c46c615 --- /dev/null +++ b/korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/StringExtensionsTest.kt @@ -0,0 +1,46 @@ +/* + * Korest Docs + * + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.lcomment.korestdocs.mockmvc.extensions + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +internal class StringExtensionsTest : DescribeSpec({ + + describe("extractPathParameters() Function Test") { + + it("should return empty list when pathVariables is null or empty") { + val urlTemplate = "/api/v1/users/{id}" + val pathVariables = emptyMap() + + val result = extractPathParameters(urlTemplate, pathVariables) + + result shouldBe emptyList() + } + + it("should return list of path variables") { + val urlTemplate = "/api/v1/users/{id}" + val pathVariables = mapOf("id" to 1) + + val result = extractPathParameters(urlTemplate, pathVariables) + + result shouldBe listOf(1) + } + } +}) diff --git a/korest-docs-starter/build.gradle.kts b/korest-docs-starter/build.gradle.kts index 49ac971..793588f 100644 --- a/korest-docs-starter/build.gradle.kts +++ b/korest-docs-starter/build.gradle.kts @@ -1,5 +1,12 @@ +import com.vanniktech.maven.publish.SonatypeHost + +plugins { + alias(libs.plugins.vanniktech.maven.publish) +} + dependencies { api(projects.core) + api(projects.mockmvc) implementation(libs.spring.web) implementation(libs.spring.test) implementation(libs.restdocs.restassured) @@ -12,3 +19,43 @@ tasks.bootJar { tasks.jar { enabled = true } + +mavenPublishing { + coordinates( + groupId = "${property("group")}", + artifactId = "${property("artifact")}-starter", + version = "${property("version")}" + ) + + pom { + name.set("korest-docs") + description.set("Spring Restdocs extension library using Kotlin Dsl") + inceptionYear.set("2025") + url.set("https://github.com/lcomment/korest-docs") + + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + + developers { + developer { + id.set("lcomment") + name.set("Hyunseok Ko") + email.set("komment.dev@gmail.com") + } + } + + scm { + connection.set("scm:git:git://github.com/lcomment/korest-docs.git") + developerConnection.set("scm:git:ssh://github.com/lcomment/korest-docs.git") + url.set("https://github.com/lcomment/korest-docs.git") + } + } + + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + + signAllPublications() +} diff --git a/libs.versions.toml b/libs.versions.toml index 94b01fe..450ff07 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -4,9 +4,6 @@ kotlin2 = "2.0.10" spring6 = "6.0.5" spring-boot3 = "3.0.3" spring-dependency-management = "1.1.4" -kotlin-jdsl = "3.5.4" -hibernate = "6.4.4.Final" -aspectjweaver = "1.9.20.1" jackson = "2.18.1" mockito = "5.14.2" testcontainers = "1.19.7" @@ -25,6 +22,9 @@ spring-dependency-management = { id = "io.spring.dependency-management", version vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } [libraries] +# jakarta +jakarta-servlet-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.0.0" } + # kotlin kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } @@ -47,18 +47,24 @@ spring-boot-starter-validation = { module = "org.springframework.boot:spring-boo spring-boot-configuration-processor = { module = "org.springframework.boot:spring-boot-configuration-processor", version.ref = "spring-boot3" } spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "spring-boot3" } +# jackson +jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jackson" } +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" } + # test spring-test = { module = "org.springframework:spring-test", version.ref = "spring6" } spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot3" } spring-security-test = { module = "org.springframework.security:spring-security-test", version.ref = "spring6" } archunit = { module = "com.tngtech.archunit:archunit-junit5", version = "1.1.0" } assertj-core = { module = "org.assertj:assertj-core", version = "3.26.3" } -junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version = "5.11.0" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version = "5.9.2" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version = "1.11.0" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.4.0" } kotest-junit = { module = "io.kotest:kotest-runner-junit5", version = "5.8.0" } +kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version = "5.8.0" } mockk = { module = "io.mockk:mockk", version = "1.13.8" } fixture-monkey-starter = { module = "com.navercorp.fixturemonkey:fixture-monkey-starter", version = "1.1.3" } testcontainers-junit = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" }