From 12561688eeb96d8ec45359d4ea89509aa83a81e6 Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 14:28:48 +0900 Subject: [PATCH 01/21] feat: remove internal and add request info --- .../lcomment/korestdocs/spec/DocumentSpec.kt | 91 +-------- .../korestdocs/spec/DocumentSpecBuilder.kt | 177 ------------------ .../lcomment/korestdocs/spec/FieldsSpec.kt | 4 +- .../korestdocs/spec/FieldsSpecBuilder.kt | 27 +-- .../lcomment/korestdocs/spec/HeadersSpec.kt | 4 +- .../korestdocs/spec/HeadersSpecBuilder.kt | 15 +- .../lcomment/korestdocs/spec/LinksSpec.kt | 2 +- .../korestdocs/spec/LinksSpecBuilder.kt | 4 +- .../korestdocs/spec/ParametersSpec.kt | 8 +- .../korestdocs/spec/ParametersSpecBuilder.kt | 8 +- .../korestdocs/spec/RequestPartsSpec.kt | 21 ++- .../spec/RequestPartsSpecBuilder.kt | 51 +++-- 12 files changed, 90 insertions(+), 322 deletions(-) delete mode 100644 korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/DocumentSpecBuilder.kt 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..57cf596 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 @@ -23,7 +23,10 @@ import kotlin.reflect.KClass import org.springframework.restdocs.request.ParameterDescriptor @RestdocsSpecDslMarker -abstract class ParametersSpec { +abstract class ParametersSpec : DocumentSpec { + + val pathVariables: MutableMap = mutableMapOf() + val queryParameters: MutableMap = mutableMapOf() inline fun pathVariable( name: String, @@ -31,6 +34,7 @@ abstract class ParametersSpec { example: T, attributes: Map = emptyMap(), ) { + pathVariables.putIfAbsent(name, example) add(name, description, example, T::class, attributes) } @@ -40,6 +44,7 @@ abstract class ParametersSpec { example: T, attributes: Map = mapOf("optional" to false), ) { + queryParameters.putIfAbsent(name, example) add(name, description, example, T::class, attributes) } @@ -49,6 +54,7 @@ abstract class ParametersSpec { example: T, attributes: Map = mapOf("optional" to true), ) { + queryParameters.putIfAbsent(name, example) add(name, description, example, T::class, attributes) } 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/ParametersSpecBuilder.kt index f9eb445..a270d74 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/ParametersSpecBuilder.kt @@ -18,16 +18,16 @@ 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 ParametersSpecBuilder( private val parameters: MutableList = mutableListOf(), ) : ParametersSpec() { 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) } } From 76bafdfb888323f24de0c03f3c09b6f4b96e56bd Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 14:29:34 +0900 Subject: [PATCH 02/21] refactor: update package --- .../KClassExtensions.kt} | 2 +- .../MapExtensions.kt} | 2 +- .../MockMvcExtensions.kt} | 2 +- .../ResultActionsExtensions.kt} | 12 +++---- .../mockmvc/extensions/StringExtensions.kt | 31 +++++++++++++++++++ 5 files changed, 40 insertions(+), 9 deletions(-) rename korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/{extension/KClassExtension.kt => extensions/KClassExtensions.kt} (97%) rename korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/{extension/MapExtension.kt => extensions/MapExtensions.kt} (97%) rename korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/{MockMvcExtension.kt => extensions/MockMvcExtensions.kt} (99%) rename korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/{ResultActionsExtension.kt => extensions/ResultActionsExtensions.kt} (79%) create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/StringExtensions.kt 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-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 79% 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..d4fae26 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,10 @@ * 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 io.github.lcomment.korestdocs.mockmvc.documentationScope import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation import org.springframework.test.web.servlet.ResultActions import org.springframework.test.web.servlet.ResultActionsDsl @@ -27,7 +27,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 +36,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 { +private 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..0c3717c --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/StringExtensions.kt @@ -0,0 +1,31 @@ +/* + * 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 String.urlMapping( + pathVariables: Map?, +): String { + var result = this + + pathVariables?.forEach { (key, value) -> + result = result.replace("{$key}", value.toString()) + } ?: result + + return result +} From 76d11dcf99b5fd9e53b8b8d572bc33cf15150894 Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 14:30:04 +0900 Subject: [PATCH 03/21] chore: disable assign workflow --- .github/workflows/auto-assign-workflow.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/auto-assign-workflow.yaml b/.github/workflows/auto-assign-workflow.yaml index d9feafa..9025368 100644 --- a/.github/workflows/auto-assign-workflow.yaml +++ b/.github/workflows/auto-assign-workflow.yaml @@ -1,10 +1,10 @@ -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 +#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 From a0fc34a194ad635bb271e79f1dfeec525b94935d Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 14:56:47 +0900 Subject: [PATCH 04/21] feat: add mockmvc context about restdocs --- .../mockmvc/context/DefaultMockMvcContext.kt | 45 +++++++++++++++++++ .../mockmvc/context/MockMvcContext.kt | 27 +++++++++++ .../mockmvc/context/MockMvcContextHolder.kt | 42 +++++++++++++++++ .../context/MockMvcContextHolderStrategy.kt | 28 ++++++++++++ ...ThreadLocalMockMvcContextHolderStrategy.kt | 45 +++++++++++++++++++ 5 files changed, 187 insertions(+) create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/DefaultMockMvcContext.kt create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContext.kt create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContextHolder.kt create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/MockMvcContextHolderStrategy.kt create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/context/ThreadLocalMockMvcContextHolderStrategy.kt 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() + } +} From 57b06f591b31e4a9fb24e505f043b3d1725b83ef Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 14:57:35 +0900 Subject: [PATCH 05/21] feat: add mockmvc document generator --- .../mockmvc/MockMvcDocumentGenerator.kt | 108 +++++++++++++ .../MockMvcDocumentGeneratorBuilder.kt | 153 ++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGenerator.kt create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGeneratorBuilder.kt 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..431edba --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGenerator.kt @@ -0,0 +1,108 @@ +/* + * 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.ParametersSpec +import io.github.lcomment.korestdocs.spec.RequestPartsSpec +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 method: HttpMethod? + + var urlTemplate: String? + + var headersBuilder: HeadersSpec? + + var parametersBuilder: ParametersSpec? + + var requestFieldsBuilder: FieldsSpec? + + fun addSnippet(snippet: Snippet) + + fun request( + method: HttpMethod, + urlTemplate: String, + configure: ParametersSpec.() -> Unit, + ) + + fun requestHeader( + attributes: Map = emptyMap(), + configure: HeadersSpec.() -> Unit, + ) + + fun pathParameter( + relaxed: Boolean = false, + attributes: Map = emptyMap(), + configure: ParametersSpec.() -> 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 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..f1a9e4b --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGeneratorBuilder.kt @@ -0,0 +1,153 @@ +/* + * 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.ParametersSpec +import io.github.lcomment.korestdocs.spec.ParametersSpecBuilder +import io.github.lcomment.korestdocs.spec.RequestPartsSpec +import io.github.lcomment.korestdocs.spec.RequestPartsSpecBuilder +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 headersBuilder: HeadersSpec? = null, + override var parametersBuilder: ParametersSpec? = null, + override var requestFieldsBuilder: FieldsSpec? = 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: ParametersSpec.() -> Unit, + ) { + this.method = method + this.urlTemplate = urlTemplate + this.parametersBuilder = ParametersSpecBuilder().apply(configure) + } + + override fun requestHeader( + attributes: Map, + configure: HeadersSpec.() -> Unit, + ) { + val specBuilder = HeadersSpecBuilder().apply(configure) + + this.headersBuilder = specBuilder + addSnippet(specBuilder.buildRequestHeaders(attributes)) + } + + override fun pathParameter( + relaxed: Boolean, + attributes: Map, + configure: ParametersSpec.() -> Unit, + ) { + val specBuilder = ParametersSpecBuilder().apply(configure) + + addSnippet(specBuilder.buildPathParameters(relaxed, attributes)) + } + + override fun requestParameter( + relaxed: Boolean, + attributes: Map, + configure: ParametersSpec.() -> Unit, + ) { + val specBuilder = ParametersSpecBuilder().apply(configure) + + addSnippet(specBuilder.buildQueryParameters(relaxed, attributes)) + } + + override fun requestField( + relaxed: Boolean, + subsectionExtractor: PayloadSubsectionExtractor<*>?, + attributes: Map, + configure: FieldsSpec.() -> Unit, + ) { + val specBuilder = FieldsSpecBuilder().apply(configure) + this.requestFieldsBuilder = specBuilder + + addSnippet(specBuilder.buildRequestFields(relaxed, subsectionExtractor, attributes)) + } + + 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 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) + } +} + +fun documentationScope(identifier: String): MockMvcDocumentGenerator = MockMvcDocumentGeneratorBuilder(identifier) From c4e5f74e52e9754a33be46493929a40ed3b1f405 Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 14:58:35 +0900 Subject: [PATCH 06/21] feat: documentation function --- .../mockmvc/MockMvcDocumentation.kt | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentation.kt 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..5fb665e --- /dev/null +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentation.kt @@ -0,0 +1,79 @@ +/* + * 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.requestWithDocs +import io.github.lcomment.korestdocs.mockmvc.extensions.urlMapping +import kotlin.collections.component1 +import kotlin.collections.component2 +import org.springframework.http.MediaType +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation +import org.springframework.test.web.servlet.ResultActionsDsl +import org.springframework.test.web.servlet.ResultHandler +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 mockMvc = MockMvcContextHolder.getContext().getMockMvc() + val headersBuilder = documentSpec.headersBuilder + val parametersBuilder = documentSpec.parametersBuilder + val requestFieldsBuilder = documentSpec.requestFieldsBuilder + val mappedUrl = urlTemplate.urlMapping(parametersBuilder?.pathVariables) + + val resultActionsDsl = mockMvc.requestWithDocs(method, mappedUrl) { + parametersBuilder?.queryParameters?.forEach { (key, value) -> + param(key, value.toString()) + } + + headersBuilder?.headers?.forEach { (key, value) -> + header(key, value.toString()) + } + + content = toJson(requestFieldsBuilder?.fields ?: emptyMap()) + contentType = MediaType.APPLICATION_JSON + } + + return resultActionsDsl.andDo { handle(documentSpec.toResultHandler()) } +} + +private fun MockMvcDocumentGenerator.toResultHandler(): ResultHandler { + return MockMvcRestDocumentation.document( + identifier, + requestPreprocessor, + responsePreprocessor, + *snippets.toTypedArray(), + ) +} + +private fun toJson(value: Any): String { + return mapper.writeValueAsString(value) +} + +private val mapper: ObjectMapper = ObjectMapper() + .registerModule(JavaTimeModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) From 06deeab1639aef77710cd762c03ee8662c12085e Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 14:59:24 +0900 Subject: [PATCH 07/21] feat: add KorestDocumentationExtension --- .../mockmvc/KorestDocumentationExtension.kt | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/KorestDocumentationExtension.kt 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() + } + } +} From 01d577e3c1118989deb47ba72f10230fe252ada1 Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 15:08:00 +0900 Subject: [PATCH 08/21] test: add example code --- .../io/github/lcomment/example/Application.kt | 18 +++ .../example/controller/ExampleController.kt | 32 +++++ .../example/dto/request/ExampleRequest.kt | 26 ++++ .../example/dto/response/ApiResponse.kt | 20 +++ .../example/dto/response/ArrayResponse.kt | 18 +++ .../example/dto/response/DateResponse.kt | 18 +++ .../example/dto/response/ExampleResponse.kt | 18 +++ .../example/dto/response/NumberResponse.kt | 18 +++ .../lcomment/example/enums/ExampleEnum.kt | 18 +++ .../lcomment/example/enums/ReturnCode.kt | 18 +++ .../example/service/ExampleService.kt | 25 ++++ .../lcomment/example/controller/ApiSpec.kt | 135 +++++++++++++++--- 12 files changed, 344 insertions(+), 20 deletions(-) create mode 100644 example/src/main/kotlin/io/github/lcomment/example/dto/request/ExampleRequest.kt 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..86fc940 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,32 @@ +/* + * 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.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.web.bind.annotation.GetMapping 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 +42,14 @@ 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() + } } 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..5c56dfc 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,65 @@ package io.github.lcomment.example.controller +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.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.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) +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 +100,89 @@ 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 `function - GET`() { + val data = ExampleResponse() + doReturn(data).`when`(exampleService).get() + + documentation("junit-get-function") { + request(HttpMethod.GET, "/example/{id}") { + pathVariable("id", "아이디", 1) + 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) + } + } + } } fun buildRestdocsMockMvc( @@ -109,3 +196,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) From f29de6265433598ab9c3161e6a4b76d54fd381b4 Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 15:08:52 +0900 Subject: [PATCH 09/21] chore: set dependency --- example/build.gradle.kts | 1 + korest-docs-core/build.gradle.kts | 47 ++++++++++++++++++++++++++++ korest-docs-mockmvc/build.gradle.kts | 4 +++ korest-docs-starter/build.gradle.kts | 47 ++++++++++++++++++++++++++++ libs.versions.toml | 9 +++--- 5 files changed, 104 insertions(+), 4 deletions(-) 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/korest-docs-core/build.gradle.kts b/korest-docs-core/build.gradle.kts index 72037e6..a12925e 100644 --- a/korest-docs-core/build.gradle.kts +++ b/korest-docs-core/build.gradle.kts @@ -1,6 +1,13 @@ +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) } tasks.bootJar { @@ -10,3 +17,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-mockmvc/build.gradle.kts b/korest-docs-mockmvc/build.gradle.kts index 9ca0977..4412889 100644 --- a/korest-docs-mockmvc/build.gradle.kts +++ b/korest-docs-mockmvc/build.gradle.kts @@ -7,6 +7,10 @@ plugins { dependencies { api(projects.core) api(libs.restdocs.mockmvc) + implementation(libs.spring.context) + implementation(libs.junit.jupiter) + implementation(libs.jackson.databind) + implementation(libs.jackson.datatype.jsr310) } tasks.bootJar { 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..4ef8873 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" @@ -47,13 +44,17 @@ 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-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" } From c9f7fb0e5ebcbf65d2cc2aa901bcc2aa5e4b52e6 Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 15:09:09 +0900 Subject: [PATCH 10/21] chore: version up --- gradle.properties | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 - - From 8754352dcae9f986671c492f8546bc61ca0fb875 Mon Sep 17 00:00:00 2001 From: lcomment Date: Wed, 26 Mar 2025 15:10:43 +0900 Subject: [PATCH 11/21] chore: add request part snippet --- .../restdocs/templates/request-part-fields.snippet | 12 ++++++++++++ .../restdocs/templates/request-parts.snippet | 10 ++++++++++ 2 files changed, 22 insertions(+) create mode 100644 korest-docs-core/src/main/resources/org/springframework/restdocs/templates/request-part-fields.snippet create mode 100644 korest-docs-core/src/main/resources/org/springframework/restdocs/templates/request-parts.snippet 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..7b7ded3 --- /dev/null +++ b/korest-docs-core/src/main/resources/org/springframework/restdocs/templates/request-parts.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Optional|Format|Description + +{{#requestParts}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{#optional}}O{{/optional}}{{^optional}}-{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/requestParts}} +|=== From 70326d67c244be688023d86f80cab2910c6f254a Mon Sep 17 00:00:00 2001 From: lcomment Date: Fri, 28 Mar 2025 11:24:11 +0900 Subject: [PATCH 12/21] fix: solve parameter mapping issue --- .../korestdocs/spec/ParametersSpec.kt | 48 +-------------- .../korestdocs/spec/PathVariablesSpec.kt | 50 ++++++++++++++++ ...Builder.kt => PathVariablesSpecBuilder.kt} | 17 ++---- .../korestdocs/spec/QueryParametersSpec.kt | 59 +++++++++++++++++++ .../spec/QueryParametersSpecBuilder.kt | 58 ++++++++++++++++++ .../mockmvc/extensions/StringExtensions.kt | 15 +++++ 6 files changed, 188 insertions(+), 59 deletions(-) create mode 100644 korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpec.kt rename korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/{ParametersSpecBuilder.kt => PathVariablesSpecBuilder.kt} (80%) create mode 100644 korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpec.kt create mode 100644 korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpecBuilder.kt 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 57cf596..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,52 +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 : DocumentSpec { - - val pathVariables: MutableMap = mutableMapOf() - val queryParameters: 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) - } - - 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) -} +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..8a2715a --- /dev/null +++ b/korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpec.kt @@ -0,0 +1,50 @@ +/* + * 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() + val queryParameters: 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 80% 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 a270d74..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 @@ -24,12 +24,11 @@ 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 -class ParametersSpecBuilder( +class PathVariablesSpecBuilder( private val parameters: MutableList = mutableListOf(), -) : ParametersSpec() { +) : PathVariablesSpec() { override fun add( name: String, @@ -52,17 +51,11 @@ 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-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 index 0c3717c..830d0d7 100644 --- 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 @@ -29,3 +29,18 @@ fun String.urlMapping( return result } + +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() +} From 072cc3d015f9f4dbc22dd342b3341f3113c2796d Mon Sep 17 00:00:00 2001 From: lcomment Date: Fri, 28 Mar 2025 11:24:54 +0900 Subject: [PATCH 13/21] feat: add multipart option in documentation function --- .../lcomment/korestdocs/type/RequestType.kt | 25 ++++ .../mockmvc/MockMvcDocumentGenerator.kt | 30 +++-- .../MockMvcDocumentGeneratorBuilder.kt | 77 ++++++++---- .../mockmvc/MockMvcDocumentation.kt | 116 ++++++++++++++---- .../mockmvc/extensions/MapExtensions.kt | 28 +++++ .../extensions/ResultActionsExtensions.kt | 2 +- 6 files changed, 221 insertions(+), 57 deletions(-) create mode 100644 korest-docs-core/src/main/kotlin/io/github/lcomment/korestdocs/type/RequestType.kt create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MapExtensions.kt 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-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 index 431edba..8a77fe5 100644 --- 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 @@ -20,8 +20,10 @@ 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.ParametersSpec +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 @@ -38,22 +40,36 @@ interface MockMvcDocumentGenerator { val snippets: List + var requestType: RequestType? + var method: HttpMethod? var urlTemplate: String? - var headersBuilder: HeadersSpec? + var headersSpec: HeadersSpec? + + var pathVariablesSpec: PathVariablesSpec? + + var queryParametersSpec: QueryParametersSpec? + + var requestFieldsSpec: FieldsSpec? - var parametersBuilder: ParametersSpec? + var requestPartsSpec: RequestPartsSpec? - var requestFieldsBuilder: FieldsSpec? + var requestPartFieldsSpec: Map? fun addSnippet(snippet: Snippet) fun request( method: HttpMethod, urlTemplate: String, - configure: ParametersSpec.() -> Unit, + configure: PathVariablesSpec.() -> Unit = {}, + ) + + fun multipart( + method: HttpMethod, + urlTemplate: String, + configure: PathVariablesSpec.() -> Unit = {}, ) fun requestHeader( @@ -64,13 +80,13 @@ interface MockMvcDocumentGenerator { fun pathParameter( relaxed: Boolean = false, attributes: Map = emptyMap(), - configure: ParametersSpec.() -> Unit, + configure: PathVariablesSpec.() -> Unit, ) fun requestParameter( relaxed: Boolean = false, attributes: Map = emptyMap(), - configure: ParametersSpec.() -> Unit, + configure: QueryParametersSpec.() -> Unit, ) fun requestField( 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 index f1a9e4b..43feb42 100644 --- 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 @@ -22,10 +22,13 @@ 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.ParametersSpec -import io.github.lcomment.korestdocs.spec.ParametersSpecBuilder +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 @@ -36,9 +39,13 @@ internal class MockMvcDocumentGeneratorBuilder( override val identifier: String, override var method: HttpMethod? = null, override var urlTemplate: String? = null, - override var headersBuilder: HeadersSpec? = null, - override var parametersBuilder: ParametersSpec? = null, - override var requestFieldsBuilder: FieldsSpec? = 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(), @@ -51,41 +58,60 @@ internal class MockMvcDocumentGeneratorBuilder( override fun request( method: HttpMethod, urlTemplate: String, - configure: ParametersSpec.() -> Unit, + configure: PathVariablesSpec.() -> Unit, ) { this.method = method this.urlTemplate = urlTemplate - this.parametersBuilder = ParametersSpecBuilder().apply(configure) + 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 specBuilder = HeadersSpecBuilder().apply(configure) + val spec = HeadersSpecBuilder().apply(configure) - this.headersBuilder = specBuilder - addSnippet(specBuilder.buildRequestHeaders(attributes)) + this.headersSpec = spec + addSnippet(spec.buildRequestHeaders(attributes)) } override fun pathParameter( relaxed: Boolean, attributes: Map, - configure: ParametersSpec.() -> Unit, + configure: PathVariablesSpec.() -> Unit, ) { - val specBuilder = ParametersSpecBuilder().apply(configure) + val spec = PathVariablesSpecBuilder().apply(configure) - addSnippet(specBuilder.buildPathParameters(relaxed, attributes)) + addSnippet(spec.buildPathParameters(relaxed, attributes)) } override fun requestParameter( relaxed: Boolean, attributes: Map, - configure: ParametersSpec.() -> Unit, + configure: QueryParametersSpec.() -> Unit, ) { - val specBuilder = ParametersSpecBuilder().apply(configure) + val spec = QueryParametersSpecBuilder().apply(configure) + this.queryParametersSpec = spec - addSnippet(specBuilder.buildQueryParameters(relaxed, attributes)) + addSnippet(spec.buildQueryParameters(relaxed, attributes)) } override fun requestField( @@ -94,10 +120,10 @@ internal class MockMvcDocumentGeneratorBuilder( attributes: Map, configure: FieldsSpec.() -> Unit, ) { - val specBuilder = FieldsSpecBuilder().apply(configure) - this.requestFieldsBuilder = specBuilder + val spec = FieldsSpecBuilder().apply(configure) + this.requestFieldsSpec = spec - addSnippet(specBuilder.buildRequestFields(relaxed, subsectionExtractor, attributes)) + addSnippet(spec.buildRequestFields(relaxed, subsectionExtractor, attributes)) } override fun requestPart( @@ -105,10 +131,10 @@ internal class MockMvcDocumentGeneratorBuilder( attributes: Map, configure: RequestPartsSpec.() -> Unit, ) { - val snippet = RequestPartsSpecBuilder().apply(configure) - .build(relaxed, attributes) + val spec = RequestPartsSpecBuilder().apply(configure) + this.requestPartsSpec = spec - addSnippet(snippet) + addSnippet(spec.build(relaxed, attributes)) } override fun requestPartField( @@ -118,11 +144,10 @@ internal class MockMvcDocumentGeneratorBuilder( attributes: Map, configure: FieldsSpec.() -> Unit, ) { - val snippet = FieldsSpecBuilder() - .apply(configure) - .buildRequestPartFields(part, relaxed, subsectionExtractor, attributes) + val spec = FieldsSpecBuilder().apply(configure) + this.requestPartFieldsSpec = mapOf(part to spec) - addSnippet(snippet) + addSnippet(spec.buildRequestPartFields(part, relaxed, subsectionExtractor, attributes)) } override fun responseHeader( 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 index 5fb665e..d0ac944 100644 --- 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 @@ -19,14 +19,20 @@ package io.github.lcomment.korestdocs.mockmvc import io.github.lcomment.korestdocs.mockmvc.context.MockMvcContextHolder +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.urlMapping -import kotlin.collections.component1 -import kotlin.collections.component2 +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.restdocs.mockmvc.MockMvcRestDocumentation +import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.ResultActionsDsl -import org.springframework.test.web.servlet.ResultHandler import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule @@ -38,39 +44,103 @@ fun documentation( 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 headersBuilder = documentSpec.headersBuilder - val parametersBuilder = documentSpec.parametersBuilder - val requestFieldsBuilder = documentSpec.requestFieldsBuilder - val mappedUrl = urlTemplate.urlMapping(parametersBuilder?.pathVariables) + 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 = mockMvc.requestWithDocs(method, mappedUrl) { - parametersBuilder?.queryParameters?.forEach { (key, value) -> + 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()) } - headersBuilder?.headers?.forEach { (key, value) -> + headersSpec?.headers?.forEach { (key, value) -> header(key, value.toString()) } - content = toJson(requestFieldsBuilder?.fields ?: emptyMap()) + content = toJson(requestFieldsSpec?.fields ?: emptyMap()) contentType = MediaType.APPLICATION_JSON } - - return resultActionsDsl.andDo { handle(documentSpec.toResultHandler()) } } -private fun MockMvcDocumentGenerator.toResultHandler(): ResultHandler { - return MockMvcRestDocumentation.document( - identifier, - requestPreprocessor, - responsePreprocessor, - *snippets.toTypedArray(), - ) +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 + } + } } -private fun toJson(value: Any): String { +fun toJson(value: Any): String { return mapper.writeValueAsString(value) } 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/ResultActionsExtensions.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/ResultActionsExtensions.kt index d4fae26..db0df77 100644 --- a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/ResultActionsExtensions.kt +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/ResultActionsExtensions.kt @@ -43,7 +43,7 @@ fun ResultActionsDsl.andDocument( return andDo { handle(documentSpec.toResultHandler()) } } -private fun MockMvcDocumentGenerator.toResultHandler(): ResultHandler { +internal fun MockMvcDocumentGenerator.toResultHandler(): ResultHandler { return MockMvcRestDocumentation.document( identifier, requestPreprocessor, From dca26050b6eee4a101e2ef2f73e31cf46db6a8de Mon Sep 17 00:00:00 2001 From: lcomment Date: Fri, 28 Mar 2025 11:25:22 +0900 Subject: [PATCH 14/21] chore: add dependency --- korest-docs-mockmvc/build.gradle.kts | 1 + libs.versions.toml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/korest-docs-mockmvc/build.gradle.kts b/korest-docs-mockmvc/build.gradle.kts index 4412889..db5c518 100644 --- a/korest-docs-mockmvc/build.gradle.kts +++ b/korest-docs-mockmvc/build.gradle.kts @@ -7,6 +7,7 @@ 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) diff --git a/libs.versions.toml b/libs.versions.toml index 4ef8873..88c906b 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -22,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" } From 97f26d2f8207a78aff559d2bd891d785d6509e70 Mon Sep 17 00:00:00 2001 From: lcomment Date: Fri, 28 Mar 2025 11:25:39 +0900 Subject: [PATCH 15/21] fix: chang request parts snippet --- .../springframework/restdocs/templates/request-parts.snippet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 7b7ded3..5b2d0fe 100644 --- 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 @@ -1,5 +1,5 @@ |=== -|Name|Optional|Format|Description +|Name|Optional|Description {{#requestParts}} |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} From a9d4a1056fdd945ce002e8bf70c831919195e0b8 Mon Sep 17 00:00:00 2001 From: lcomment Date: Fri, 28 Mar 2025 11:25:49 +0900 Subject: [PATCH 16/21] test: add multipart test --- .../example/controller/ExampleController.kt | 15 ++++ .../dto/request/ExampleMultipartRequest.kt | 10 +++ .../lcomment/example/controller/ApiSpec.kt | 73 ++++++++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 example/src/main/kotlin/io/github/lcomment/example/dto/request/ExampleMultipartRequest.kt 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 86fc940..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 @@ -18,12 +18,15 @@ 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 @@ -52,4 +55,16 @@ class ExampleController( 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/test/kotlin/io/github/lcomment/example/controller/ApiSpec.kt b/example/src/test/kotlin/io/github/lcomment/example/controller/ApiSpec.kt index 5c56dfc..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,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.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 @@ -18,9 +37,11 @@ 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.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.setup.DefaultMockMvcBuilder import org.springframework.test.web.servlet.setup.MockMvcBuilders @@ -32,7 +53,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule @SpringBootTest @ExtendWith(KorestDocumentationExtension::class) -class ApiSpec { +internal class ApiSpec { lateinit var mockMvc: MockMvc @@ -122,6 +143,37 @@ class ApiSpec { } } + @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() @@ -130,6 +182,9 @@ class ApiSpec { documentation("junit-get-function") { request(HttpMethod.GET, "/example/{id}") { pathVariable("id", "아이디", 1) + } + + requestParameter { queryParameter("param1", "파라미터 1", "value1") } @@ -183,6 +238,22 @@ class ApiSpec { } } } + + @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( From 54b51dec60298cde55e82cac60429c461afbffa3 Mon Sep 17 00:00:00 2001 From: lcomment Date: Tue, 1 Apr 2025 13:32:26 +0900 Subject: [PATCH 17/21] chore: set kotest dependencies --- korest-docs-core/build.gradle.kts | 3 +++ korest-docs-mockmvc/build.gradle.kts | 4 ++++ libs.versions.toml | 2 ++ 3 files changed, 9 insertions(+) diff --git a/korest-docs-core/build.gradle.kts b/korest-docs-core/build.gradle.kts index a12925e..0dd11b7 100644 --- a/korest-docs-core/build.gradle.kts +++ b/korest-docs-core/build.gradle.kts @@ -8,6 +8,9 @@ dependencies { implementation(libs.restdocs.core) implementation(libs.spring.web) implementation(libs.spring.test) + + testImplementation(libs.kotest.junit) + testImplementation(libs.kotest.assertions.core) } tasks.bootJar { diff --git a/korest-docs-mockmvc/build.gradle.kts b/korest-docs-mockmvc/build.gradle.kts index db5c518..60ce128 100644 --- a/korest-docs-mockmvc/build.gradle.kts +++ b/korest-docs-mockmvc/build.gradle.kts @@ -12,6 +12,10 @@ dependencies { 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/libs.versions.toml b/libs.versions.toml index 88c906b..450ff07 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -48,6 +48,7 @@ spring-boot-configuration-processor = { module = "org.springframework.boot:sprin 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" } @@ -63,6 +64,7 @@ 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" } From aff7bd7105cd3318ff352739a0462c77ff5936ce Mon Sep 17 00:00:00 2001 From: lcomment Date: Tue, 1 Apr 2025 13:32:42 +0900 Subject: [PATCH 18/21] refactor: remove unused method --- .../lcomment/korestdocs/spec/PathVariablesSpec.kt | 1 - .../mockmvc/extensions/StringExtensions.kt | 12 ------------ 2 files changed, 13 deletions(-) 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 index 8a2715a..16b01b8 100644 --- 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 @@ -26,7 +26,6 @@ import org.springframework.restdocs.request.ParameterDescriptor abstract class PathVariablesSpec : ParametersSpec { val pathVariables: MutableMap = mutableMapOf() - val queryParameters: MutableMap = mutableMapOf() inline fun pathVariable( name: String, 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 index 830d0d7..722ea43 100644 --- 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 @@ -18,18 +18,6 @@ package io.github.lcomment.korestdocs.mockmvc.extensions -fun String.urlMapping( - pathVariables: Map?, -): String { - var result = this - - pathVariables?.forEach { (key, value) -> - result = result.replace("{$key}", value.toString()) - } ?: result - - return result -} - fun extractPathParameters( urlTemplate: String, pathVariables: Map?, From 9baf42272d5645b1a2f15b4214d3879f8cc39663 Mon Sep 17 00:00:00 2001 From: lcomment Date: Tue, 1 Apr 2025 14:31:23 +0900 Subject: [PATCH 19/21] refactor: separate extension function --- .../korestdocs/mockmvc/MockMvcDocumentGeneratorBuilder.kt | 3 +-- .../lcomment/korestdocs/mockmvc/MockMvcDocumentation.kt | 1 + .../extensions/MockMvcDocumentGeneratorExtensions.kt | 8 ++++++++ .../mockmvc/extensions/ResultActionsExtensions.kt | 1 - 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MockMvcDocumentGeneratorExtensions.kt 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 index 43feb42..f713ad1 100644 --- 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 @@ -99,6 +99,7 @@ internal class MockMvcDocumentGeneratorBuilder( configure: PathVariablesSpec.() -> Unit, ) { val spec = PathVariablesSpecBuilder().apply(configure) + this.pathVariablesSpec = spec addSnippet(spec.buildPathParameters(relaxed, attributes)) } @@ -174,5 +175,3 @@ internal class MockMvcDocumentGeneratorBuilder( addSnippet(snippet) } } - -fun documentationScope(identifier: String): MockMvcDocumentGenerator = MockMvcDocumentGeneratorBuilder(identifier) 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 index d0ac944..a1770ca 100644 --- 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 @@ -19,6 +19,7 @@ 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 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/extensions/ResultActionsExtensions.kt b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/ResultActionsExtensions.kt index db0df77..2902cf4 100644 --- a/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/ResultActionsExtensions.kt +++ b/korest-docs-mockmvc/src/main/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/ResultActionsExtensions.kt @@ -19,7 +19,6 @@ package io.github.lcomment.korestdocs.mockmvc.extensions import io.github.lcomment.korestdocs.mockmvc.MockMvcDocumentGenerator -import io.github.lcomment.korestdocs.mockmvc.documentationScope import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation import org.springframework.test.web.servlet.ResultActions import org.springframework.test.web.servlet.ResultActionsDsl From 7d1925ea5867de6b66170d5892996ae1c7722639 Mon Sep 17 00:00:00 2001 From: lcomment Date: Tue, 1 Apr 2025 14:31:35 +0900 Subject: [PATCH 20/21] test: add unit test --- .../extensions/KClassExtensionsTest.kt | 205 ++++++++++++++++++ .../extensions/MapExtensionsTest.kt | 116 ++++++++++ .../korestdocs/spec/FieldsSpecTest.kt | 100 +++++++++ .../korestdocs/spec/HeadersSpecTest.kt | 63 ++++++ .../lcomment/korestdocs/spec/LinksSpecTest.kt | 41 ++++ .../korestdocs/spec/PathVariablesSpecTest.kt | 44 ++++ .../spec/QueryParametersSpecTest.kt | 63 ++++++ .../korestdocs/spec/RequestPartsSpecTest.kt | 64 ++++++ .../mockmvc/MockMvcDocumentGeneratorTest.kt | 130 +++++++++++ .../mockmvc/extensions/MapExtensionsTest.kt | 46 ++++ .../extensions/StringExtensionsTest.kt | 46 ++++ 11 files changed, 918 insertions(+) create mode 100644 korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/extensions/KClassExtensionsTest.kt create mode 100644 korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/extensions/MapExtensionsTest.kt create mode 100644 korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/FieldsSpecTest.kt create mode 100644 korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/HeadersSpecTest.kt create mode 100644 korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/LinksSpecTest.kt create mode 100644 korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/PathVariablesSpecTest.kt create mode 100644 korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/QueryParametersSpecTest.kt create mode 100644 korest-docs-core/src/test/kotlin/io/github/lcomment/korestdocs/spec/RequestPartsSpecTest.kt create mode 100644 korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/MockMvcDocumentGeneratorTest.kt create mode 100644 korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/MapExtensionsTest.kt create mode 100644 korest-docs-mockmvc/src/test/kotlin/io/github/lcomment/korestdocs/mockmvc/extensions/StringExtensionsTest.kt 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/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) + } + } +}) From 603026597838d0f11d3a95d26219b97ed5718a5e Mon Sep 17 00:00:00 2001 From: lcomment Date: Tue, 1 Apr 2025 14:40:44 +0900 Subject: [PATCH 21/21] chore: delete auto assign workflow --- .github/workflows/auto-assign-workflow.yaml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 .github/workflows/auto-assign-workflow.yaml diff --git a/.github/workflows/auto-assign-workflow.yaml b/.github/workflows/auto-assign-workflow.yaml deleted file mode 100644 index 9025368..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