diff --git a/.travis.yml b/.travis.yml index 6048d603..e4a80dfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ dist: focal before_install: - - sudo apt-get install -y openjdk-8-jdk + - sudo apt-get install -y openjdk-17-jdk before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ @@ -25,7 +25,7 @@ env: global: - CI_NAME=travis-ci - SIGNING_KEYRING_FILE="${TRAVIS_BUILD_DIR}/secret-keys.gpg" - - JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 + - JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 # coveralls - secure: "nOkY2AcR5Z7hKKzi+zCzmzPk+xiRRrJjGliDJnHnEO7g+iJ8SmT+thHuMFcXqvjxJ006LbR3GPB8M8qgFXFDPEUDRPppShi3waOm1ddlM2iXh9kbCoROoioyRO+F07deH6ExibkLbu/xq3WwAAw7lx/ZP+buc2R2VCbV0nLKP9iE4nHQX8msLgmU1LjWf0Au1Pgl2eWXV0J4/ZJOvcVu9hvBf1Ow4C7BNb0KZmMjeT9uMK0hiGpb9VVbwedOWfsbaFmqmKYqVKS8UtNmG+HBjvZfajrIARKRPc9w9uEvfl4E1H/J7PJLy2kOrCOauj8LMu6DduSrWcg08T5VnH51cavSAsWWG1ImTgEsUBNpllc9r1XLhQCLQ1TWCgGmleqYjZ2ySvg0MQpFjTgXMZC+aDkjZjcEeq3BgrncAR/bcG4ByrZBWyoEXDxBwoMZnkryTxK07UUgXRXdjJUldJ5CQW9oSfd+oEXKJyqQGNt0ob3S0sRS5uhrKniK+6Mwzxf6vXeYvn2fVP5j3hocUz4XzDJQoDDmJi6D4BpwksqJ81CNOHpyeDIOhdXxvA/J8DMCK0Q7tTSbnSOxItaht56CSxqqLSGax3/4Nr+5QX9jpmdJ03HdpP+MOuIl6dZCmE4/w1IFaFEfhpETPlnWZHl1BsOt7omDeW7yCttDq3Y1mXU=" # gradle plugin portal diff --git a/build.gradle.kts b/build.gradle.kts index 44aa29fe..bd4a91c4 100755 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ plugins { id("pl.allegro.tech.build.axion-release") version "1.9.2" jacoco java - kotlin("jvm") version "1.4.20" apply false + kotlin("jvm") version "1.7.22" apply false `maven-publish` } @@ -58,12 +58,12 @@ allprojects { subprojects { val jacksonVersion by extra { "2.12.2" } - val springBootVersion by extra { "2.1.9.RELEASE" } - val springRestDocsVersion by extra { "2.0.4.RELEASE" } + val springBootVersion by extra { "3.0.2" } + val springRestDocsVersion by extra { "3.0.0" } val junitVersion by extra { "5.4.2" } tasks.withType { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "17" } tasks.withType { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a..943f0cbf 100755 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 92f06b50..2b22d057 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..65dcd68d 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -205,6 +209,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..6689b85b 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/ApiSpecTaskTest.kt b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/ApiSpecTaskTest.kt index b58d3865..630d2f84 100644 --- a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/ApiSpecTaskTest.kt +++ b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/ApiSpecTaskTest.kt @@ -102,7 +102,8 @@ abstract class ApiSpecTaskTest { "contentType" : null, "headers" : [ ], "pathParameters" : [ ], - "requestParameters" : [ ], + "queryParameters" : [ ], + "formParameters" : [ ], "requestFields" : [ ], "example" : null, "securityRequirements" : { @@ -146,7 +147,8 @@ abstract class ApiSpecTaskTest { "default" : "a default value" } ], "pathParameters" : [ ], - "requestParameters" : [ ], + "queryParameters" : [ ], + "formParameters" : [ ], "requestFields" : [ ], "example" : null, "securityRequirements" : { @@ -182,7 +184,8 @@ abstract class ApiSpecTaskTest { "contentType" : null, "headers" : [ ], "pathParameters" : [ ], - "requestParameters" : [ ], + "queryParameters" : [ ], + "formParameters" : [ ], "requestFields" : [ ], "example" : null, "securityRequirements" : null diff --git a/restdocs-api-spec-mockmvc/build.gradle.kts b/restdocs-api-spec-mockmvc/build.gradle.kts index 0f207f94..273e49b1 100644 --- a/restdocs-api-spec-mockmvc/build.gradle.kts +++ b/restdocs-api-spec-mockmvc/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { exclude("junit") } + testImplementation("org.springframework.boot:spring-boot-starter-validation:$springBootVersion") testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion") testImplementation("org.junit-pioneer:junit-pioneer:0.3.0") testImplementation("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion") diff --git a/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt b/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt index 224f40e0..16c4d1d8 100644 --- a/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt +++ b/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt @@ -2,6 +2,7 @@ package com.epages.restdocs.apispec import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName import com.epages.restdocs.apispec.ResourceDocumentation.resource +import jakarta.validation.constraints.NotEmpty import org.hibernate.validator.constraints.Length import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.SpringApplication @@ -9,9 +10,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.ConfigurableApplicationContext +import org.springframework.hateoas.EntityModel +import org.springframework.hateoas.IanaLinkRelations import org.springframework.hateoas.Link -import org.springframework.hateoas.Resource -import org.springframework.hateoas.mvc.BasicLinkBuilder +import org.springframework.hateoas.server.mvc.BasicLinkBuilder.linkToCurrentMapping import org.springframework.http.HttpHeaders.ACCEPT import org.springframework.http.HttpHeaders.CONTENT_TYPE import org.springframework.http.ResponseEntity @@ -26,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RestController import java.util.UUID -import javax.validation.constraints.NotEmpty @ExtendWith(SpringExtension::class) @WebMvcTest @@ -53,17 +54,17 @@ open class ResourceSnippetIntegrationTest { @PathVariable otherId: Int?, @RequestHeader("X-Custom-Header") customHeader: String, @RequestBody testDataHolder: TestDataHolder - ): ResponseEntity> { - val resource = Resource(testDataHolder.copy(id = UUID.randomUUID().toString())) - val link = BasicLinkBuilder.linkToCurrentMapping().slash("some").slash(someId).slash("other").slash(otherId).toUri().toString() - resource.add(Link(link, Link.REL_SELF)) - resource.add(Link(link, "multiple")) - resource.add(Link(link, "multiple")) + ): ResponseEntity> { + val resource = EntityModel.of(testDataHolder.copy(id = UUID.randomUUID().toString())) + val link = linkToCurrentMapping().slash("some").slash(someId).slash("other").slash(otherId).toUri().toString() + resource.add(Link.of(link, IanaLinkRelations.SELF)) + resource.add(Link.of(link, "multiple")) + resource.add(Link.of(link, "multiple")) return ResponseEntity .ok() .header("X-Custom-Header", customHeader) - .body>(resource) + .body>(resource) } } } diff --git a/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt b/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt index c7605f9e..50580667 100644 --- a/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt +++ b/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt @@ -37,7 +37,8 @@ data class RequestModel( val securityRequirements: SecurityRequirements?, val headers: List, val pathParameters: List, - val requestParameters: List, + val queryParameters: List, + val formParameters: List, val requestFields: List, val example: String? = null, val schema: Schema? = null diff --git a/restdocs-api-spec-openapi-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20Generator.kt b/restdocs-api-spec-openapi-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20Generator.kt index 2d0c4772..03895eab 100644 --- a/restdocs-api-spec-openapi-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20Generator.kt +++ b/restdocs-api-spec-openapi-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20Generator.kt @@ -288,14 +288,12 @@ object OpenApi20Generator { firstModelForPathAndMethod ).plus( modelsWithSamePathAndMethod - .filter { it.request.contentType != "application/x-www-form-urlencoded" } - .flatMap { it.request.requestParameters } + .flatMap { it.request.queryParameters } .distinctBy { it.name } .map { requestParameterDescriptor2QueryParameter(it) } ).plus( modelsWithSamePathAndMethod - .filter { it.request.contentType == "application/x-www-form-urlencoded" } - .flatMap { it.request.requestParameters } + .flatMap { it.request.formParameters } .distinctBy { it.name } .map { requestParameterDescriptor2FormParameter(it) } ).plus( @@ -327,6 +325,7 @@ object OpenApi20Generator { SecurityType.OAUTH2 -> addSecurity(OAUTH2_SECURITY_NAME, securityRequirements2ScopesList(securityRequirements)) SecurityType.BASIC -> addSecurity(BASIC_SECURITY_NAME, null) SecurityType.API_KEY -> addSecurity(API_KEY_SECURITY_NAME, null) + SecurityType.JWT_BEARER -> { /* not specified for OpenApi 2.0 */ } } } } diff --git a/restdocs-api-spec-openapi-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20GeneratorTest.kt b/restdocs-api-spec-openapi-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20GeneratorTest.kt index 251fd4a0..13707fcd 100644 --- a/restdocs-api-spec-openapi-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20GeneratorTest.kt +++ b/restdocs-api-spec-openapi-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi2/OpenApi20GeneratorTest.kt @@ -424,14 +424,14 @@ class OpenApi20GeneratorTest { val productResourceModel = api[0] val productPath = openapi.paths.getValue(productResourceModel.request.path) - thenParameterMatches(productPath.post.parameters as List>, "formData", productResourceModel.request.requestParameters[0]) + thenParameterMatches(productPath.post.parameters as List>, "formData", productResourceModel.request.formParameters[0]) } private fun thenPutRequestShouldHaveFormDataParameters(openapi: Swagger, api: List) { val productResourceModel = api[0] val productPath = openapi.paths.getValue(productResourceModel.request.path) - thenParameterMatches(productPath.put.parameters as List>, "formData", productResourceModel.request.requestParameters[0]) + thenParameterMatches(productPath.put.parameters as List>, "formData", productResourceModel.request.formParameters[0]) } private fun thenGetProductWith400ResponseIsGenerated(openapi: Swagger, api: List) { @@ -447,16 +447,16 @@ class OpenApi20GeneratorTest { private fun thenParametersForGetMatch(parameters: List>, request: RequestModel) { thenParameterMatches(parameters, "path", request.pathParameters[0]) - thenParameterMatches(parameters, "query", request.requestParameters[0]) + thenParameterMatches(parameters, "query", request.queryParameters[0]) thenParameterMatches(parameters, "header", request.headers[0]) } private fun thenParametersForGetMatchWithDefaultValue(parameters: List>, request: RequestModel) { thenParameterMatches(parameters, "path", request.pathParameters[0]) - thenParameterMatches(parameters, "query", request.requestParameters[0]) - thenParameterMatches(parameters, "query", request.requestParameters[1]) - thenParameterMatches(parameters, "query", request.requestParameters[2]) - thenParameterMatches(parameters, "query", request.requestParameters[3]) + thenParameterMatches(parameters, "query", request.queryParameters[0]) + thenParameterMatches(parameters, "query", request.queryParameters[1]) + thenParameterMatches(parameters, "query", request.queryParameters[2]) + thenParameterMatches(parameters, "query", request.queryParameters[3]) thenParameterMatches(parameters, "header", request.headers[0]) thenParameterMatches(parameters, "header", request.headers[1]) } @@ -465,10 +465,10 @@ class OpenApi20GeneratorTest { thenParameterMatches(parameters, "path", request.pathParameters[0]) thenParameterEnumValuesMatches(parameters, "header", request.headers[0]) thenParameterEnumValuesMatches(parameters, "header", request.headers[1]) - thenParameterEnumValuesMatches(parameters, "query", request.requestParameters[0]) - thenParameterEnumValuesMatches(parameters, "query", request.requestParameters[1]) - thenParameterEnumValuesMatches(parameters, "query", request.requestParameters[2]) - thenParameterEnumValuesMatches(parameters, "query", request.requestParameters[3]) + thenParameterEnumValuesMatches(parameters, "query", request.queryParameters[0]) + thenParameterEnumValuesMatches(parameters, "query", request.queryParameters[1]) + thenParameterEnumValuesMatches(parameters, "query", request.queryParameters[2]) + thenParameterEnumValuesMatches(parameters, "query", request.queryParameters[3]) } private fun thenParametersForPostMatch(parameters: List>, request: RequestModel) { @@ -904,7 +904,7 @@ class OpenApi20GeneratorTest { ignored = false ) ), - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = "locale", description = "Localizes the product fields to the given locale code", @@ -913,13 +913,14 @@ class OpenApi20GeneratorTest { ignored = false ) ), + formParameters = listOf(), requestFields = listOf() ) } private fun getProductRequestWithDifferentParameter(name: String, description: String): RequestModel { return getProductRequest().copy( - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = name, description = description, @@ -940,7 +941,8 @@ class OpenApi20GeneratorTest { ), headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), requestFields = listOf() ) } @@ -956,7 +958,8 @@ class OpenApi20GeneratorTest { ), headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), requestFields = listOf() ) } @@ -972,7 +975,8 @@ class OpenApi20GeneratorTest { ), headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), requestFields = listOf() ) } @@ -995,7 +999,7 @@ class OpenApi20GeneratorTest { defaultValue = true ) ), - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = "booleanParameter", description = "a boolean parameter", @@ -1054,7 +1058,7 @@ class OpenApi20GeneratorTest { ) ) ), - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = "booleanParameter", description = "a boolean parameter", @@ -1108,7 +1112,8 @@ class OpenApi20GeneratorTest { securityRequirements = null, headers = listOf(), pathParameters = listOf(), - requestParameters = listOf( + queryParameters = listOf(), + formParameters = listOf( ParameterDescriptor( name = "locale", description = "Localizes the product fields to the given locale code", @@ -1143,7 +1148,8 @@ class OpenApi20GeneratorTest { ) ), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), requestFields = listOf( FieldDescriptor( path = "description", @@ -1189,7 +1195,8 @@ class OpenApi20GeneratorTest { ignored = false ) ), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), requestFields = listOf() ) } @@ -1204,7 +1211,8 @@ class OpenApi20GeneratorTest { ), headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), requestFields = listOf() ) } @@ -1219,7 +1227,8 @@ class OpenApi20GeneratorTest { ), headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), requestFields = listOf() ) } diff --git a/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt b/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt index 54071f16..4e812956 100644 --- a/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt +++ b/restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt @@ -236,8 +236,7 @@ object OpenApi3Generator { firstModelForPathAndMethod ).plus( modelsWithSamePathAndMethod - .filter { it.request.contentType != "application/x-www-form-urlencoded" } - .flatMap { it.request.requestParameters } + .flatMap { it.request.queryParameters } .distinctBy { it.name } .map { requestParameterDescriptor2Parameter(it) } ).plus( @@ -291,7 +290,7 @@ object OpenApi3Generator { toMediaType( requestFields = requests.flatMap { it -> if (it.request.contentType == "application/x-www-form-urlencoded") { - it.request.requestParameters.map { parameterDescriptor2FieldDescriptor(it) } + it.request.formParameters.map { parameterDescriptor2FieldDescriptor(it) } } else { it.request.requestFields } diff --git a/restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt b/restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt index a0393330..cd669e55 100644 --- a/restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt +++ b/restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt @@ -911,7 +911,8 @@ class OpenApi3GeneratorTest { method = HTTPMethod.DELETE, headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), securityRequirements = null, requestFields = listOf() ), @@ -1085,7 +1086,8 @@ class OpenApi3GeneratorTest { method = HTTPMethod.PATCH, headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), schema = schema, securityRequirements = null, requestFields = listOf( @@ -1114,7 +1116,8 @@ class OpenApi3GeneratorTest { method = HTTPMethod.PATCH, headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), securityRequirements = null, requestFields = listOf( FieldDescriptor( @@ -1153,7 +1156,8 @@ class OpenApi3GeneratorTest { securityRequirements = getSecurityRequirement(), headers = emptyList(), pathParameters = emptyList(), - requestParameters = emptyList(), + queryParameters = emptyList(), + formParameters = emptyList(), requestFields = listOf() ) } @@ -1166,7 +1170,8 @@ class OpenApi3GeneratorTest { securityRequirements = getSecurityRequirement(), headers = emptyList(), pathParameters = emptyList(), - requestParameters = listOf( + queryParameters = listOf(), + formParameters = listOf( ParameterDescriptor( name = "locale", description = "Localizes the product fields to the given locale code", @@ -1206,7 +1211,7 @@ class OpenApi3GeneratorTest { ignored = false ) ), - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = "locale", description = "Localizes the product fields to the given locale code", @@ -1215,6 +1220,7 @@ class OpenApi3GeneratorTest { ignored = false ) ), + formParameters = listOf(), requestFields = listOf() ) } @@ -1230,7 +1236,7 @@ class OpenApi3GeneratorTest { private fun getProductRequestWithDifferentParameter(name: String, description: String): RequestModel { return getProductRequest().copy( - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = name, description = description, @@ -1309,7 +1315,7 @@ class OpenApi3GeneratorTest { defaultValue = 2L ) ), - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = "booleanParameter", description = "a boolean parameter", @@ -1388,7 +1394,7 @@ class OpenApi3GeneratorTest { private fun getProductRequestWithRequestParameterWithWrongDefaultValue(): RequestModel { return getProductRequest().copy( - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = "booleanParameter", description = "a boolean parameter", @@ -1458,7 +1464,7 @@ class OpenApi3GeneratorTest { ) ) ), - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = "booleanParameter", description = "a boolean parameter", @@ -1500,6 +1506,7 @@ class OpenApi3GeneratorTest { ) ) ), + formParameters = listOf(), pathParameters = listOf(), requestFields = listOf() ) @@ -1507,7 +1514,7 @@ class OpenApi3GeneratorTest { private fun getProductRequestWithRequestParameterWithWrongEnumValues(): RequestModel { return getProductRequest().copy( - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = "integerParameter", description = "a integer parameter", diff --git a/restdocs-api-spec-postman-generator/src/main/kotlin/com/epages/restdocs/apispec/postman/PostmanCollectionGenerator.kt b/restdocs-api-spec-postman-generator/src/main/kotlin/com/epages/restdocs/apispec/postman/PostmanCollectionGenerator.kt index 92b24127..6406293a 100644 --- a/restdocs-api-spec-postman-generator/src/main/kotlin/com/epages/restdocs/apispec/postman/PostmanCollectionGenerator.kt +++ b/restdocs-api-spec-postman-generator/src/main/kotlin/com/epages/restdocs/apispec/postman/PostmanCollectionGenerator.kt @@ -111,7 +111,7 @@ object PostmanCollectionGenerator { } .ifEmpty { null } query = modelsWithSamePathAndMethod - .flatMap { it.request.requestParameters } + .flatMap { it.request.queryParameters } .distinctBy { it.name } .map { Query().apply { diff --git a/restdocs-api-spec-postman-generator/src/test/kotlin/com/epages/restdocs/apispec/postman/PostmanCollectionGeneratorTest.kt b/restdocs-api-spec-postman-generator/src/test/kotlin/com/epages/restdocs/apispec/postman/PostmanCollectionGeneratorTest.kt index 115882fc..9356dede 100644 --- a/restdocs-api-spec-postman-generator/src/test/kotlin/com/epages/restdocs/apispec/postman/PostmanCollectionGeneratorTest.kt +++ b/restdocs-api-spec-postman-generator/src/test/kotlin/com/epages/restdocs/apispec/postman/PostmanCollectionGeneratorTest.kt @@ -217,7 +217,8 @@ internal class PostmanCollectionGeneratorTest { method = HTTPMethod.DELETE, headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), securityRequirements = null, requestFields = listOf() ), @@ -326,7 +327,8 @@ internal class PostmanCollectionGeneratorTest { method = HTTPMethod.PATCH, headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), securityRequirements = null, requestFields = listOf( FieldDescriptor( @@ -348,7 +350,8 @@ internal class PostmanCollectionGeneratorTest { method = HTTPMethod.PATCH, headers = listOf(), pathParameters = listOf(), - requestParameters = listOf(), + queryParameters = listOf(), + formParameters = listOf(), securityRequirements = null, requestFields = listOf( FieldDescriptor( @@ -406,7 +409,7 @@ internal class PostmanCollectionGeneratorTest { ignored = false ) ), - requestParameters = listOf( + queryParameters = listOf( ParameterDescriptor( name = "locale", description = "Localizes the product fields to the given locale code", @@ -415,6 +418,7 @@ internal class PostmanCollectionGeneratorTest { ignored = false ) ), + formParameters = listOf(), requestFields = listOf() ) } diff --git a/restdocs-api-spec-restassured/build.gradle.kts b/restdocs-api-spec-restassured/build.gradle.kts index 757829e5..d1130d35 100644 --- a/restdocs-api-spec-restassured/build.gradle.kts +++ b/restdocs-api-spec-restassured/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { } testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion") testImplementation("org.junit-pioneer:junit-pioneer:0.3.0") + testImplementation("org.springframework.boot:spring-boot-starter-validation:$springBootVersion") testImplementation("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion") } diff --git a/restdocs-api-spec-restassured/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt b/restdocs-api-spec-restassured/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt index 1232a90e..c53afa29 100644 --- a/restdocs-api-spec-restassured/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt +++ b/restdocs-api-spec-restassured/src/main/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapper.kt @@ -2,8 +2,8 @@ package com.epages.restdocs.apispec import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor -import org.springframework.restdocs.restassured3.RestAssuredRestDocumentation -import org.springframework.restdocs.restassured3.RestDocumentationFilter +import org.springframework.restdocs.restassured.RestAssuredRestDocumentation +import org.springframework.restdocs.restassured.RestDocumentationFilter import org.springframework.restdocs.snippet.Snippet import java.util.function.Function diff --git a/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt index faa69c28..2bd23aad 100644 --- a/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt +++ b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt @@ -2,6 +2,7 @@ package com.epages.restdocs.apispec import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName import com.epages.restdocs.apispec.ResourceDocumentation.resource +import jakarta.validation.constraints.NotEmpty import org.hibernate.validator.constraints.Length import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -11,9 +12,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.ConfigurableApplicationContext +import org.springframework.hateoas.EntityModel +import org.springframework.hateoas.IanaLinkRelations import org.springframework.hateoas.Link -import org.springframework.hateoas.Resource -import org.springframework.hateoas.mvc.BasicLinkBuilder +import org.springframework.hateoas.server.mvc.BasicLinkBuilder.linkToCurrentMapping import org.springframework.http.HttpHeaders.ACCEPT import org.springframework.http.HttpHeaders.CONTENT_TYPE import org.springframework.http.ResponseEntity @@ -29,7 +31,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RestController import java.util.UUID -import javax.validation.constraints.NotEmpty @ExtendWith(SpringExtension::class) @WebMvcTest @@ -71,17 +72,17 @@ open class ResourceSnippetIntegrationTest { @PathVariable otherId: Int?, @RequestHeader("X-Custom-Header") customHeader: String, @RequestBody testDataHolder: TestDataHolder - ): ResponseEntity> { - val resource = Resource(testDataHolder.copy(id = UUID.randomUUID().toString())) - val link = BasicLinkBuilder.linkToCurrentMapping().slash("some").slash(someId).slash("other").slash(otherId).toUri().toString() - resource.add(Link(link, Link.REL_SELF)) - resource.add(Link(link, "multiple")) - resource.add(Link(link, "multiple")) + ): ResponseEntity> { + val resource = EntityModel.of(testDataHolder.copy(id = UUID.randomUUID().toString())) + val link = linkToCurrentMapping().slash("some").slash(someId).slash("other").slash(otherId).toUri().toString() + resource.add(Link.of(link, IanaLinkRelations.SELF)) + resource.add(Link.of(link, "multiple")) + resource.add(Link.of(link, "multiple")) return ResponseEntity .ok() .header("X-Custom-Header", customHeader) - .body>(resource) + .body>(resource) } } } diff --git a/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt index 96d411a8..16879616 100644 --- a/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt +++ b/restdocs-api-spec-restassured/src/test/kotlin/com/epages/restdocs/apispec/RestAssuredRestDocumentationWrapperIntegrationTest.kt @@ -25,8 +25,8 @@ import org.springframework.restdocs.payload.PayloadDocumentation.responseFields import org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath import org.springframework.restdocs.request.RequestDocumentation.parameterWithName import org.springframework.restdocs.request.RequestDocumentation.pathParameters -import org.springframework.restdocs.restassured3.RestAssuredRestDocumentation -import org.springframework.restdocs.restassured3.RestDocumentationFilter +import org.springframework.restdocs.restassured.RestAssuredRestDocumentation +import org.springframework.restdocs.restassured.RestDocumentationFilter import java.io.File @ExtendWith(RestDocumentationExtension::class) diff --git a/restdocs-api-spec-webtestclient/build.gradle.kts b/restdocs-api-spec-webtestclient/build.gradle.kts index 2a595567..0913253d 100644 --- a/restdocs-api-spec-webtestclient/build.gradle.kts +++ b/restdocs-api-spec-webtestclient/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { exclude("junit") } + testImplementation("org.hibernate.validator:hibernate-validator:8.0.0.Final") + testImplementation("org.springframework.boot:spring-boot-starter-validation:$springBootVersion") testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion") testImplementation("org.junit-pioneer:junit-pioneer:0.3.0") testImplementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion") diff --git a/restdocs-api-spec-webtestclient/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt b/restdocs-api-spec-webtestclient/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt index 9d7d30b8..944fb11f 100644 --- a/restdocs-api-spec-webtestclient/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt +++ b/restdocs-api-spec-webtestclient/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetIntegrationTest.kt @@ -2,6 +2,7 @@ package com.epages.restdocs.apispec import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName import com.epages.restdocs.apispec.ResourceDocumentation.resource +import jakarta.validation.constraints.NotEmpty import org.hibernate.validator.constraints.Length import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.SpringApplication @@ -24,7 +25,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RestController import java.util.UUID -import javax.validation.constraints.NotEmpty @ExtendWith(SpringExtension::class) @AutoConfigureRestDocs diff --git a/restdocs-api-spec/build.gradle.kts b/restdocs-api-spec/build.gradle.kts index 30fd1fde..51b20b75 100755 --- a/restdocs-api-spec/build.gradle.kts +++ b/restdocs-api-spec/build.gradle.kts @@ -18,6 +18,8 @@ dependencies { implementation(kotlin("reflect")) implementation("org.springframework.restdocs:spring-restdocs-core:$springRestDocsVersion") + implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion") + implementation("org.springframework.boot:spring-boot-starter-validation:$springBootVersion") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/DescriptorValidator.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/DescriptorValidator.kt index 96924d91..5941db40 100644 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/DescriptorValidator.kt +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/DescriptorValidator.kt @@ -12,10 +12,11 @@ import org.springframework.restdocs.payload.FieldDescriptor import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.RequestFieldsSnippet import org.springframework.restdocs.payload.ResponseFieldsSnippet +import org.springframework.restdocs.request.FormParametersSnippet import org.springframework.restdocs.request.ParameterDescriptor import org.springframework.restdocs.request.PathParametersSnippet +import org.springframework.restdocs.request.QueryParametersSnippet import org.springframework.restdocs.request.RequestDocumentation.parameterWithName -import org.springframework.restdocs.request.RequestParametersSnippet internal object DescriptorValidator { @@ -46,14 +47,23 @@ internal object DescriptorValidator { ) ) } - validateIfDescriptorsPresent( - requestParameters, + queryParameters, + operation + ) { + QueryParameterSnippetWrapper( + toParameterDescriptors( + queryParameters + ) + ) + } + validateIfDescriptorsPresent( + formParameters, operation ) { - RequestParameterSnippetWrapper( + FormParameterSnippetWrapper( toParameterDescriptors( - requestParameters + formParameters ) ) } @@ -159,8 +169,16 @@ internal object DescriptorValidator { } } - private class RequestParameterSnippetWrapper(descriptors: List) : - RequestParametersSnippet(descriptors), + private class FormParameterSnippetWrapper(descriptors: List) : + FormParametersSnippet(descriptors), + ValidateableSnippet { + override fun validate(operation: Operation) { + super.createModel(operation) + } + } + + private class QueryParameterSnippetWrapper(descriptors: List) : + QueryParametersSnippet(descriptors), ValidateableSnippet { override fun validate(operation: Operation) { super.createModel(operation) diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippet.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippet.kt index f2d9aa62..b138e4cd 100755 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippet.kt +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippet.kt @@ -67,11 +67,12 @@ class ResourceSnippet(private val resourceSnippetParameters: ResourceSnippetPara tags = tags, request = RequestModel( path = getUriPath(operation), - method = operation.request.method.name, + method = operation.request.method.name(), contentType = if (hasRequestBody) getContentTypeOrDefault(operation.request.headers) else null, headers = resourceSnippetParameters.requestHeaders.withExampleValues(operation.request.headers), pathParameters = resourceSnippetParameters.pathParameters.filter { !it.isIgnored }, - requestParameters = resourceSnippetParameters.requestParameters.filter { !it.isIgnored }, + queryParameters = resourceSnippetParameters.queryParameters.filter { !it.isIgnored }, + formParameters = resourceSnippetParameters.formParameters.filter { !it.isIgnored }, schema = resourceSnippetParameters.requestSchema, requestFields = if (hasRequestBody) resourceSnippetParameters.requestFields.filter { !it.isIgnored } else emptyList(), example = if (hasRequestBody) operation.request.contentAsString else null, @@ -135,7 +136,8 @@ class ResourceSnippet(private val resourceSnippetParameters: ResourceSnippetPara val schema: Schema? = null, val headers: List, val pathParameters: List, - val requestParameters: List, + val queryParameters: List, + val formParameters: List, val requestFields: List, val example: String?, val securityRequirements: SecurityRequirements? diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippetParameters.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippetParameters.kt index 39634f50..6e6efa5d 100755 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippetParameters.kt +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippetParameters.kt @@ -26,7 +26,8 @@ data class ResourceSnippetParameters @JvmOverloads constructor( val responseFields: List = emptyList(), val links: List = emptyList(), val pathParameters: List = emptyList(), - val requestParameters: List = emptyList(), + val queryParameters: List = emptyList(), + val formParameters: List = emptyList(), val requestHeaders: List = emptyList(), val responseHeaders: List = emptyList(), val tags: Set = emptySet() @@ -200,7 +201,9 @@ class ResourceSnippetParametersBuilder : ResourceSnippetDetails() { private set var pathParameters: List = emptyList() private set - var requestParameters: List = emptyList() + var queryParameters: List = emptyList() + private set + var formParameters: List = emptyList() private set var requestHeaders: List = emptyList() private set @@ -233,14 +236,22 @@ class ResourceSnippetParametersBuilder : ResourceSnippetDetails() { } ) - fun requestParameters(vararg requestParameters: ParameterDescriptorWithType) = requestParameters(requestParameters.toList()) - fun requestParameters(requestParameters: List) = apply { this.requestParameters = requestParameters } - fun requestParameters(vararg requestParameters: ParameterDescriptor) = requestParameters( + fun queryParameters(vararg requestParameters: ParameterDescriptorWithType) = queryParameters(requestParameters.toList()) + fun queryParameters(requestParameters: List) = apply { this.queryParameters = requestParameters } + fun queryParameters(vararg requestParameters: ParameterDescriptor) = queryParameters( requestParameters.map { ParameterDescriptorWithType.fromParameterDescriptor(it) } ) + fun formParameters(vararg formParameters: ParameterDescriptorWithType) = formParameters(formParameters.toList()) + fun formParameters(formParameters: List) = apply { this.formParameters = formParameters } + fun formParameters(vararg formParameters: ParameterDescriptor) = formParameters( + formParameters.map { + ParameterDescriptorWithType.fromParameterDescriptor(it) + } + ) + fun requestHeaders(requestHeaders: List) = apply { this.requestHeaders = requestHeaders } fun requestHeaders(vararg requestHeaders: HeaderDescriptorWithType) = requestHeaders(requestHeaders.toList()) fun requestHeaders(vararg requestHeaders: HeaderDescriptor) = @@ -270,7 +281,8 @@ class ResourceSnippetParametersBuilder : ResourceSnippetDetails() { responseFields, links, pathParameters, - requestParameters, + queryParameters, + formParameters, requestHeaders, responseHeaders, tags diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt index dabae9e0..642ac61c 100644 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/RestDocumentationWrapper.kt @@ -8,9 +8,10 @@ import org.springframework.restdocs.hypermedia.LinksSnippet import org.springframework.restdocs.payload.FieldDescriptor import org.springframework.restdocs.payload.RequestFieldsSnippet import org.springframework.restdocs.payload.ResponseFieldsSnippet +import org.springframework.restdocs.request.FormParametersSnippet import org.springframework.restdocs.request.ParameterDescriptor import org.springframework.restdocs.request.PathParametersSnippet -import org.springframework.restdocs.request.RequestParametersSnippet +import org.springframework.restdocs.request.QueryParametersSnippet import org.springframework.restdocs.snippet.Snippet import java.util.function.Function @@ -48,8 +49,17 @@ abstract class RestDocumentationWrapper { ) } ) - .requestParameters( - *snippets.filter { it is RequestParametersSnippet } + .queryParameters( + *snippets.filter { it is QueryParametersSnippet } + .flatMap { + DescriptorExtractor.extractDescriptorsFor( + it + ) + } + .toTypedArray() + ) + .formParameters( + *snippets.filter { it is FormParametersSnippet } .flatMap { DescriptorExtractor.extractDescriptorsFor( it diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ConstrainedFieldsTest.kt b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ConstrainedFieldsTest.kt index 9b0d64b6..dd6de8e9 100644 --- a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ConstrainedFieldsTest.kt +++ b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ConstrainedFieldsTest.kt @@ -1,9 +1,9 @@ package com.epages.restdocs.apispec +import jakarta.validation.constraints.NotEmpty import org.assertj.core.api.BDDAssertions.then import org.junit.jupiter.api.Test import org.springframework.restdocs.constraints.Constraint -import javax.validation.constraints.NotEmpty internal class ConstrainedFieldsTest { diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/OperationBuilder.kt b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/OperationBuilder.kt index 5668d0e4..b402623a 100644 --- a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/OperationBuilder.kt +++ b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/OperationBuilder.kt @@ -13,7 +13,6 @@ import org.springframework.restdocs.operation.OperationRequestPart import org.springframework.restdocs.operation.OperationRequestPartFactory import org.springframework.restdocs.operation.OperationResponse import org.springframework.restdocs.operation.OperationResponseFactory -import org.springframework.restdocs.operation.Parameters import org.springframework.restdocs.operation.StandardOperation import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory import org.springframework.restdocs.snippet.StandardWriterResolver @@ -23,10 +22,9 @@ import org.springframework.restdocs.templates.TemplateEngine import org.springframework.restdocs.templates.TemplateFormats import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine +import org.springframework.web.util.UriComponentsBuilder import java.io.File import java.net.URI -import java.util.ArrayList -import java.util.HashMap /** * Helper class to support testing snippets by providing a builder for the central Operation class @@ -140,8 +138,6 @@ class OperationBuilder { private val headers = HttpHeaders() - private val parameters = Parameters() - private val partBuilders = ArrayList() init { @@ -155,7 +151,7 @@ class OperationBuilder { } return OperationRequestFactory().create( this.requestUri, this.method, - this.content, this.headers, this.parameters, parts + this.content, this.headers, parts ) } @@ -178,13 +174,9 @@ class OperationBuilder { return this } - fun param(name: String, vararg values: String): OperationRequestBuilder { + fun queryParam(name: String, vararg values: String): OperationRequestBuilder { if (values.isNotEmpty()) { - for (value in values) { - this.parameters.add(name, value) - } - } else { - this.parameters[name] = emptyList() + this.requestUri = UriComponentsBuilder.fromUri(requestUri).queryParam(name, values).build().toUri() } return this } diff --git a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetTest.kt b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetTest.kt index bde7088f..d45f2a41 100644 --- a/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetTest.kt +++ b/restdocs-api-spec/src/test/kotlin/com/epages/restdocs/apispec/ResourceSnippetTest.kt @@ -14,6 +14,7 @@ import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.http.HttpHeaders.CONTENT_TYPE import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus.OK +import org.springframework.http.MediaType import org.springframework.http.MediaType.APPLICATION_JSON_VALUE import org.springframework.restdocs.generate.RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE import org.springframework.restdocs.headers.HeaderDocumentation @@ -69,7 +70,7 @@ class ResourceSnippetTest { givenResponseFieldDescriptors() givenResponseSchemaName() givenPathParameterDescriptors() - givenRequestParameterDescriptors() + givenQueryParameterDescriptors() givenRequestAndResponseHeaderDescriptors() givenTag() @@ -99,13 +100,13 @@ class ResourceSnippetTest { then(resourceSnippetJson.read("request.pathParameters[0].optional")).isFalse() then(resourceSnippetJson.read("request.pathParameters[0].ignored")).isFalse() - then(resourceSnippetJson.read>("request.requestParameters")).hasSize(1) - then(resourceSnippetJson.read("request.requestParameters[0].name")).isNotEmpty() - then(resourceSnippetJson.read("request.requestParameters[0].description")).isNotEmpty() - then(resourceSnippetJson.read("request.requestParameters[0].type")).isNotEmpty() - then(resourceSnippetJson.read("request.requestParameters[0].default")).isNotEmpty() - then(resourceSnippetJson.read("request.requestParameters[0].optional")).isFalse() - then(resourceSnippetJson.read("request.requestParameters[0].ignored")).isFalse() + then(resourceSnippetJson.read>("request.queryParameters")).hasSize(1) + then(resourceSnippetJson.read("request.queryParameters[0].name")).isNotEmpty() + then(resourceSnippetJson.read("request.queryParameters[0].description")).isNotEmpty() + then(resourceSnippetJson.read("request.queryParameters[0].type")).isNotEmpty() + then(resourceSnippetJson.read("request.queryParameters[0].default")).isNotEmpty() + then(resourceSnippetJson.read("request.queryParameters[0].optional")).isFalse() + then(resourceSnippetJson.read("request.queryParameters[0].ignored")).isFalse() then(resourceSnippetJson.read>("request.securityRequirements.requiredScopes")).containsExactly("scope1", "scope2") then(resourceSnippetJson.read("request.securityRequirements.type")).isEqualTo("OAUTH2") @@ -126,6 +127,28 @@ class ResourceSnippetTest { then(resourceSnippetJson.read("response.headers[0].example")).isNotEmpty() } + @Test + fun should_generate_resourcemodel_for_form_request_and_response_body() { + givenOperationWithRequestAndResponseBody( + responseContentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + content = "test-param=1" + ) + givenFormParameterDescriptors() + + whenResourceSnippetInvoked() + + thenSnippetFileExists() + thenSnippetFileHasCommonRequestAttributes() + + then(resourceSnippetJson.read>("request.formParameters")).hasSize(1) + then(resourceSnippetJson.read("request.formParameters[0].name")).isNotEmpty() + then(resourceSnippetJson.read("request.formParameters[0].description")).isNotEmpty() + then(resourceSnippetJson.read("request.formParameters[0].type")).isNotEmpty() + then(resourceSnippetJson.read("request.formParameters[0].default")).isNotEmpty() + then(resourceSnippetJson.read("request.formParameters[0].optional")).isFalse() + then(resourceSnippetJson.read("request.formParameters[0].ignored")).isFalse() + } + @Test fun should_generate_resourcemodel_for_operation_without_body() { givenOperationWithoutBody() @@ -149,22 +172,34 @@ class ResourceSnippetTest { } @Test - fun should_filter_ignored_parameters() { - givenOperationWithRequestParameters() - givenIgnoredAndNotIgnoredRequestParameterDescriptors() + fun should_filter_ignored_query_parameters() { + givenOperationWithQueryParameters() + givenIgnoredAndNotIgnoredQueryParameterDescriptors() whenResourceSnippetInvoked() thenSnippetFileExists() - then(resourceSnippetJson.read>("request.requestParameters")).hasSize(1) - then(resourceSnippetJson.read("request.requestParameters[0].name")).isEqualTo("describedParameter") + then(resourceSnippetJson.read>("request.queryParameters")).hasSize(1) + then(resourceSnippetJson.read("request.queryParameters[0].name")).isEqualTo("describedParameter") + } + + @Test + fun should_filter_ignored_form_parameters() { + givenOperationWithFormParameters() + givenIgnoredAndNotIgnoredFormParameterDescriptors() + + whenResourceSnippetInvoked() + + thenSnippetFileExists() + then(resourceSnippetJson.read>("request.formParameters")).hasSize(1) + then(resourceSnippetJson.read("request.formParameters[0].name")).isEqualTo("describedParameter") } @Test fun should_generate_parameter_attributes() { givenOperationWithPathAndRequestParametersHasAttributes() givenPathParameterDescriptorsHasAttributes() - givenRequestParameterDescriptorsHasAttributes() + givenQueryParameterDescriptorsHasAttributes() whenResourceSnippetInvoked() @@ -182,16 +217,42 @@ class ResourceSnippetTest { listOf("T1", "T2", "T3") ) - then(resourceSnippetJson.read>("request.requestParameters")).hasSize(2) - then(resourceSnippetJson.read("request.requestParameters[0].name")).isEqualTo("numberParameter") - then(resourceSnippetJson.read("request.requestParameters[0].type")).isEqualTo(SimpleType.INTEGER.name) - then(resourceSnippetJson.read("request.requestParameters[0].description")).isEqualTo("number") - then(resourceSnippetJson.read("request.requestParameters[0].optional")).isFalse - then(resourceSnippetJson.read("request.requestParameters[1].name")).isEqualTo("categoryParameter") - then(resourceSnippetJson.read("request.requestParameters[1].type")).isEqualTo(SimpleType.STRING.name) - then(resourceSnippetJson.read("request.requestParameters[1].description")).isEqualTo("category enum string") - then(resourceSnippetJson.read("request.requestParameters[1].optional")).isFalse - then(resourceSnippetJson.read>("request.requestParameters[1].attributes.enumValues")).isEqualTo( + then(resourceSnippetJson.read>("request.queryParameters")).hasSize(2) + then(resourceSnippetJson.read("request.queryParameters[0].name")).isEqualTo("numberParameter") + then(resourceSnippetJson.read("request.queryParameters[0].type")).isEqualTo(SimpleType.INTEGER.name) + then(resourceSnippetJson.read("request.queryParameters[0].description")).isEqualTo("number") + then(resourceSnippetJson.read("request.queryParameters[0].optional")).isFalse + then(resourceSnippetJson.read("request.queryParameters[1].name")).isEqualTo("categoryParameter") + then(resourceSnippetJson.read("request.queryParameters[1].type")).isEqualTo(SimpleType.STRING.name) + then(resourceSnippetJson.read("request.queryParameters[1].description")).isEqualTo("category enum string") + then(resourceSnippetJson.read("request.queryParameters[1].optional")).isFalse + then(resourceSnippetJson.read>("request.queryParameters[1].attributes.enumValues")).isEqualTo( + listOf("C1", "C2", "C3") + ) + } + + @Test + fun should_generate_form_parameter_attributes() { + givenOperationWithRequestAndResponseBody( + responseContentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + content = "numberParameter=21&categoryParameter=C2" + ) + givenFormParameterDescriptorsHasAttributes() + + whenResourceSnippetInvoked() + + thenSnippetFileExists() + + then(resourceSnippetJson.read>("request.formParameters")).hasSize(2) + then(resourceSnippetJson.read("request.formParameters[0].name")).isEqualTo("numberParameter") + then(resourceSnippetJson.read("request.formParameters[0].type")).isEqualTo(SimpleType.INTEGER.name) + then(resourceSnippetJson.read("request.formParameters[0].description")).isEqualTo("number") + then(resourceSnippetJson.read("request.formParameters[0].optional")).isFalse + then(resourceSnippetJson.read("request.formParameters[1].name")).isEqualTo("categoryParameter") + then(resourceSnippetJson.read("request.formParameters[1].type")).isEqualTo(SimpleType.STRING.name) + then(resourceSnippetJson.read("request.formParameters[1].description")).isEqualTo("category enum string") + then(resourceSnippetJson.read("request.formParameters[1].optional")).isFalse + then(resourceSnippetJson.read>("request.formParameters[1].attributes.enumValues")).isEqualTo( listOf("C1", "C2", "C3") ) } @@ -259,8 +320,12 @@ class ResourceSnippetTest { parametersBuilder.pathParameters(parameterWithName("id").description("an id")) } - private fun givenRequestParameterDescriptors() { - parametersBuilder.requestParameters(parameterWithName("test-param").type(SimpleType.STRING).defaultValue("default-value").description("test param")) + private fun givenQueryParameterDescriptors() { + parametersBuilder.queryParameters(parameterWithName("test-param").type(SimpleType.STRING).defaultValue("default-value").description("test param")) + } + + private fun givenFormParameterDescriptors() { + parametersBuilder.formParameters(parameterWithName("test-param").type(SimpleType.STRING).defaultValue("default-value").description("test param")) } private fun givenRequestAndResponseHeaderDescriptors() { @@ -344,15 +409,33 @@ class ResourceSnippetTest { operation = operationBuilder.build() } - private fun givenOperationWithRequestParameters() { + private fun givenOperationWithQueryParameters() { + val operationBuilder = OperationBuilder("test", rootOutputDirectory) + + operationBuilder + .attribute(ATTRIBUTE_NAME_URL_TEMPLATE, "http://localhost:8080/some/{id}") + .request("http://localhost:8080/some/123") + .queryParam("describedParameter", "will", "be", "documented") + .queryParam("obviousParameter", "wont", "be", "documented") + .method("GET") + + operationBuilder + .response() + .status(204) + + operation = operationBuilder.build() + } + + private fun givenOperationWithFormParameters() { val operationBuilder = OperationBuilder("test", rootOutputDirectory) operationBuilder .attribute(ATTRIBUTE_NAME_URL_TEMPLATE, "http://localhost:8080/some/{id}") .request("http://localhost:8080/some/123") - .param("describedParameter", "will", "be", "documented") - .param("obviousParameter", "wont", "be", "documented") .method("GET") + .content( + "describedParameter=will,be,documented&obviousParameter=wont,be,documented" + ) operationBuilder .response() @@ -367,8 +450,8 @@ class ResourceSnippetTest { operationBuilder .attribute(ATTRIBUTE_NAME_URL_TEMPLATE, "http://localhost:8080/some/{no}/{type}") .request("http://localhost:8080/some/123/T1") - .param("numberParameter", "21") - .param("categoryParameter", "C2") + .queryParam("numberParameter", "21") + .queryParam("categoryParameter", "C2") .method("GET") operationBuilder @@ -408,8 +491,15 @@ class ResourceSnippetTest { ) } - private fun givenIgnoredAndNotIgnoredRequestParameterDescriptors() { - parametersBuilder.requestParameters( + private fun givenIgnoredAndNotIgnoredQueryParameterDescriptors() { + parametersBuilder.queryParameters( + parameterWithName("describedParameter").description("description"), + parameterWithName("obviousParameter").description("needs no documentation, too obvious").ignored() + ) + } + + private fun givenIgnoredAndNotIgnoredFormParameterDescriptors() { + parametersBuilder.formParameters( parameterWithName("describedParameter").description("description"), parameterWithName("obviousParameter").description("needs no documentation, too obvious").ignored() ) @@ -424,8 +514,17 @@ class ResourceSnippetTest { ) } - private fun givenRequestParameterDescriptorsHasAttributes() { - parametersBuilder.requestParameters( + private fun givenQueryParameterDescriptorsHasAttributes() { + parametersBuilder.queryParameters( + parameterWithName("numberParameter").type(SimpleType.INTEGER).description("number"), + parameterWithName("categoryParameter").description("category enum string").attributes( + Attributes.key("enumValues").value(arrayOf("C1", "C2", "C3")) + ) + ) + } + + private fun givenFormParameterDescriptorsHasAttributes() { + parametersBuilder.formParameters( parameterWithName("numberParameter").type(SimpleType.INTEGER).description("number"), parameterWithName("categoryParameter").description("category enum string").attributes( Attributes.key("enumValues").value(arrayOf("C1", "C2", "C3")) @@ -433,13 +532,15 @@ class ResourceSnippetTest { ) } - private fun givenOperationWithRequestAndResponseBody(responseContentType: String = APPLICATION_JSON_VALUE) { + private fun givenOperationWithRequestAndResponseBody( + responseContentType: String = APPLICATION_JSON_VALUE, + content: String = "{\"comment\": \"some\"}" + ) { val operationBuilder = OperationBuilder("test", rootOutputDirectory) .attribute(ATTRIBUTE_NAME_URL_TEMPLATE, "http://localhost:8080/some/{id}") - val content = "{\"comment\": \"some\"}" operationBuilder .request("http://localhost:8080/some/123") - .param("test-param", "1") + .queryParam("test-param", "1") .method("POST") .header("X-SOME", "some") .header(AUTHORIZATION, "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJzY29wZTEiLCJzY29wZTIiXSwiZXhwIjoxNTA3NzU4NDk4LCJpYXQiOjE1MDc3MTUyOTgsImp0aSI6IjQyYTBhOTFhLWQ2ZWQtNDBjYy1iMTA2LWU5MGNkYWU0M2Q2ZCJ9.eWGo7Y124_Hdrr-bKX08d_oCfdgtlGXo9csz-hvRhRORJi_ZK7PIwM0ChqoLa4AhR-dJ86npid75GB9IxCW2f5E24FyZW2p5swpOpfkEAA4oFuj7jxHiaiqL_HFKKCRsVNAN3hGiSp9Hn3fde0-LlABqMaihdzZzHL-xm8-CqbXT-qBfuscDImZrZQZqhizpSEV4idbEMzZykggLASGoOIL0t0ycfe3yeuQkMUhzZmXuu08VM7zXwWnqfXCa-RmA6wC7ZnWqiJoi0vBr4BrlLR067YoUrT6pgRfiy2HZ0vEE_XY5SBtA-qI2QnlJb7eTk7pgFtoGkYdeOZ86k6GDVw")