From 2258dc0e1228d56812201b03d01b8261b151a9c3 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 13:07:06 +0900 Subject: [PATCH 01/13] feat: Add requestParts spec --- .../com/epages/restdocs/apispec/gradle/ApiSpecTaskTest.kt | 3 +++ .../kotlin/com/epages/restdocs/apispec/ResourceSnippet.kt | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) 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 630d2f84..ea8c2018 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 @@ -105,6 +105,7 @@ abstract class ApiSpecTaskTest { "queryParameters" : [ ], "formParameters" : [ ], "requestFields" : [ ], + "requestParts" : [ ], "example" : null, "securityRequirements" : { "type": "OAUTH2", @@ -150,6 +151,7 @@ abstract class ApiSpecTaskTest { "queryParameters" : [ ], "formParameters" : [ ], "requestFields" : [ ], + "requestParts" : [ ], "example" : null, "securityRequirements" : { "type": "OAUTH2", @@ -187,6 +189,7 @@ abstract class ApiSpecTaskTest { "queryParameters" : [ ], "formParameters" : [ ], "requestFields" : [ ], + "requestParts" : [ ], "example" : null, "securityRequirements" : null }, 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 b138e4cd..d2701ec0 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 @@ -8,6 +8,7 @@ import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.restdocs.RestDocumentationContext import org.springframework.restdocs.generate.RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE import org.springframework.restdocs.operation.Operation +import org.springframework.restdocs.operation.OperationRequestPart import org.springframework.restdocs.payload.FieldDescriptor import org.springframework.restdocs.snippet.PlaceholderResolverFactory import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory @@ -48,6 +49,7 @@ class ResourceSnippet(private val resourceSnippetParameters: ResourceSnippetPara val hasRequestBody = operation.request.contentAsString.isNotEmpty() val hasResponseBody = operation.response.contentAsString.isNotEmpty() + val hasPart = operation.request.parts.isNotEmpty() val securityRequirements = SecurityRequirementsHandler().extractSecurityRequirements(operation) @@ -68,13 +70,14 @@ class ResourceSnippet(private val resourceSnippetParameters: ResourceSnippetPara request = RequestModel( path = getUriPath(operation), method = operation.request.method.name(), - contentType = if (hasRequestBody) getContentTypeOrDefault(operation.request.headers) else null, + contentType = if (hasRequestBody || hasPart) getContentTypeOrDefault(operation.request.headers) else null, headers = resourceSnippetParameters.requestHeaders.withExampleValues(operation.request.headers), pathParameters = resourceSnippetParameters.pathParameters.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(), + requestParts = operation.request.parts.toList(), example = if (hasRequestBody) operation.request.contentAsString else null, securityRequirements = securityRequirements ), @@ -139,6 +142,7 @@ class ResourceSnippet(private val resourceSnippetParameters: ResourceSnippetPara val queryParameters: List, val formParameters: List, val requestFields: List, + val requestParts: List, val example: String?, val securityRequirements: SecurityRequirements? ) From d961de7b2e48f094a1b69f1c6f3ce23daf1ff389 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 13:07:47 +0900 Subject: [PATCH 02/13] feat: Add FileSchema --- .../apispec/jsonschema/schema/FileSchema.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/schema/FileSchema.kt diff --git a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/schema/FileSchema.kt b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/schema/FileSchema.kt new file mode 100644 index 00000000..a4e00bd5 --- /dev/null +++ b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/schema/FileSchema.kt @@ -0,0 +1,50 @@ +package com.epages.restdocs.apispec.jsonschema.schema + +import org.everit.json.schema.EmptySchema +import org.everit.json.schema.internal.JSONPrinter + +class FileSchema( + builder: BinarySchemaBuilder +) : EmptySchema(builder) { + + val format: String = builder.format + class BinarySchemaBuilder : EmptySchema.Builder() { + + internal var format: String = "binary" + + override fun build(): FileSchema { + return FileSchema(this) + } + + fun format(format : String) : BinarySchemaBuilder { + this.format = format + return this + } + } + + companion object { + @JvmStatic + fun builder(): BinarySchemaBuilder { + return BinarySchemaBuilder() + } + } + + override fun describeTo(writer: JSONPrinter) { + writer.`object`() + writer.ifPresent("title", super.getTitle()) + writer.ifPresent("description", super.getDescription()) + writer.ifPresent("id", super.getId()) + writer.ifPresent("default", super.getDefaultValue()) + writer.ifPresent("nullable", super.isNullable()) + writer.ifPresent("readOnly", super.isReadOnly()) + writer.ifPresent("writeOnly", super.isWriteOnly()) + writer.key("type").value("string") + writer.key("format").value(format) + super.getUnprocessedProperties().forEach { (key: String?, `val`: Any?) -> + writer.key( + key + ).value(`val`) + } + writer.endObject() + } +} From aa9ac517b4ba05efb0ab19a2df0d8adb6cd685f3 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 13:08:19 +0900 Subject: [PATCH 03/13] feat: Add typeToSchema file type --- .../jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt index 56f7adb6..79ad9489 100644 --- a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt +++ b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt @@ -8,6 +8,7 @@ import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.maybeMinSizeArr import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.maybePattern import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.minInteger import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.minLengthString +import com.epages.restdocs.apispec.jsonschema.schema.FileSchema import com.epages.restdocs.apispec.model.Attributes import com.epages.restdocs.apispec.model.FieldDescriptor import com.fasterxml.jackson.databind.SerializationFeature @@ -265,6 +266,7 @@ class JsonSchemaFromFieldDescriptorsGenerator { EnumSchema.builder().possibleValues(this.attributes.enumValues).build() ) ).isSynthetic(true) + "file" -> FileSchema.builder() else -> throw IllegalArgumentException("unknown field type $type") } From 0bb5e18cdbb51f354d1fdef23c673081cd0ed532 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 13:08:50 +0900 Subject: [PATCH 04/13] feat: Add RequestPartFieldDescriptor --- .../restdocs/apispec/model/ResourceModel.kt | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) 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 0cbb6aba..e619f60c 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 @@ -1,7 +1,7 @@ package com.epages.restdocs.apispec.model +import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty -import java.util.Comparator data class ResourceModel( val operationId: String, @@ -40,6 +40,7 @@ data class RequestModel( val queryParameters: List, val formParameters: List, val requestFields: List, + val requestParts: List, val example: String? = null, val schema: Schema? = null ) @@ -88,6 +89,47 @@ open class FieldDescriptor( val attributes: Attributes = Attributes() ) +open class RequestPartFieldDescriptor( + val name: String, + val submittedFileName: String?, + val content: ByteArray, + val headers: RequestPartHeaderDescriptor, + val optional: Boolean = false, + val ignored: Boolean = false, + val attributes: Attributes = Attributes() +) { + companion object { + private val FILE_TYPES = listOf( + "application/msword", + "application/pdf", + "application/vnd.ms-excel", + "application/x-javascript", + "application/zip", + "image/jpeg", + "image/jpg", + "image/jpe", + "text/css", + "text/html", + "text/htm", + "text/plain", + "text/xml", + "text/xsl" + ) + } + val type: String = when { + headers.contentType?.any { it in FILE_TYPES } == true -> "file" + else -> "string" + } +} + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class RequestPartHeaderDescriptor( + @JsonProperty("Content-Type") + val contentType: List?, + @JsonProperty("Content-Length") + val contentLength: List?, +) + data class Attributes( val validationConstraints: List = emptyList(), val enumValues: List = emptyList(), From 30c9e27da446bd2d2178b1629c033b26e78223d3 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 13:11:54 +0900 Subject: [PATCH 05/13] feat: Support OpenApi3Generator requestParts --- .../apispec/openapi3/OpenApi3Generator.kt | 20 ++++++++++++++++++- .../apispec/openapi3/OpenApi3GeneratorTest.kt | 16 +++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) 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 54d59186..5c67235c 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 @@ -7,6 +7,7 @@ import com.epages.restdocs.apispec.model.HTTPMethod import com.epages.restdocs.apispec.model.HeaderDescriptor import com.epages.restdocs.apispec.model.Oauth2Configuration import com.epages.restdocs.apispec.model.ParameterDescriptor +import com.epages.restdocs.apispec.model.RequestPartFieldDescriptor import com.epages.restdocs.apispec.model.RequestModel import com.epages.restdocs.apispec.model.ResourceModel import com.epages.restdocs.apispec.model.ResponseModel @@ -329,7 +330,11 @@ object OpenApi3Generator { requestFields = requests.flatMap { it -> if (it.request.contentType == "application/x-www-form-urlencoded") { it.request.formParameters.map { parameterDescriptor2FieldDescriptor(it) } - } else { + } + else if (it.request.contentType == "multipart/form-data") { + it.request.requestParts.map { partDescriptor2FieldDescriptor(it) } + } + else { it.request.requestFields } }, @@ -443,6 +448,19 @@ object OpenApi3Generator { ) } + private fun partDescriptor2FieldDescriptor( + partDescriptor: RequestPartFieldDescriptor + ): FieldDescriptor { + return FieldDescriptor( + path = partDescriptor.name, + description = partDescriptor.submittedFileName ?: "", + type = partDescriptor.type, + optional = partDescriptor.optional, + ignored = partDescriptor.optional, + attributes = partDescriptor.attributes + ) + } + private fun pathParameterDescriptor2Parameter(parameterDescriptor: ParameterDescriptor): PathParameter { return PathParameter().apply { name = parameterDescriptor.name 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 c135aaca..c845cefb 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 @@ -928,7 +928,8 @@ class OpenApi3GeneratorTest { queryParameters = listOf(), formParameters = listOf(), securityRequirements = null, - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ), response = ResponseModel( status = 204, @@ -1185,6 +1186,7 @@ class OpenApi3GeneratorTest { attributes = Attributes(enumValues = listOf("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE")) ) ), + requestParts = listOf(), contentType = "application/json", example = """{ "description": "Good stuff!", @@ -1218,6 +1220,7 @@ class OpenApi3GeneratorTest { type = "STRING" ) ), + requestParts = listOf(), contentType = "application/json-patch+json", example = """ [ @@ -1263,6 +1266,7 @@ class OpenApi3GeneratorTest { type = "STRING" ), ), + requestParts = listOf(), contentType = "application/json", example = """ { @@ -1286,7 +1290,8 @@ class OpenApi3GeneratorTest { pathParameters = emptyList(), queryParameters = emptyList(), formParameters = emptyList(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ) } @@ -1310,6 +1315,7 @@ class OpenApi3GeneratorTest { ), schema = schema, requestFields = listOf(), + requestParts = listOf(), example = """ locale=pl&irrelevant=true """.trimIndent() @@ -1349,7 +1355,8 @@ class OpenApi3GeneratorTest { ) ), formParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf(), ) } @@ -1636,7 +1643,8 @@ class OpenApi3GeneratorTest { ), formParameters = listOf(), pathParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf(), ) } From 7a1f0cc5103533ffc57dd6b0d6bfa72ce62f2321 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 13:12:15 +0900 Subject: [PATCH 06/13] feat: Support OpenApi20Generator requestParts --- .../apispec/openapi2/OpenApi20Generator.kt | 18 ++++++++++++++- .../openapi2/OpenApi20GeneratorTest.kt | 23 +++++++++++++------ 2 files changed, 33 insertions(+), 8 deletions(-) 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 03895eab..4fce8338 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 @@ -6,6 +6,7 @@ import com.epages.restdocs.apispec.model.HTTPMethod import com.epages.restdocs.apispec.model.HeaderDescriptor import com.epages.restdocs.apispec.model.Oauth2Configuration import com.epages.restdocs.apispec.model.ParameterDescriptor +import com.epages.restdocs.apispec.model.RequestPartFieldDescriptor import com.epages.restdocs.apispec.model.ResourceModel import com.epages.restdocs.apispec.model.ResponseModel import com.epages.restdocs.apispec.model.Schema @@ -312,7 +313,12 @@ object OpenApi20Generator { firstModelForPathAndMethod.request.schema ) ) - ).nullIfEmpty() + ).plus( + modelsWithSamePathAndMethod + .flatMap { it.request.requestParts } + .map { part2Parameters(it) } + ) + .nullIfEmpty() responses = responsesByStatusCode( modelsWithSamePathAndMethod ) @@ -478,6 +484,16 @@ object OpenApi20Generator { } } + private fun part2Parameters(partDescriptor: RequestPartFieldDescriptor): FormParameter { + return FormParameter().apply { + name = partDescriptor.name + description = partDescriptor.submittedFileName + required = partDescriptor.optional + type = partDescriptor.type + default = partDescriptor.content + } + } + private fun responseModel2Response(responseModel: ResponseModel): Response { return Response().apply { description = "" 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 13707fcd..be338039 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 @@ -914,7 +914,8 @@ class OpenApi20GeneratorTest { ) ), formParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ) } @@ -943,7 +944,8 @@ class OpenApi20GeneratorTest { pathParameters = listOf(), queryParameters = listOf(), formParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ) } @@ -960,7 +962,8 @@ class OpenApi20GeneratorTest { pathParameters = listOf(), queryParameters = listOf(), formParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ) } @@ -977,7 +980,8 @@ class OpenApi20GeneratorTest { pathParameters = listOf(), queryParameters = listOf(), formParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ) } @@ -1123,6 +1127,7 @@ class OpenApi20GeneratorTest { ) ), requestFields = listOf(), + requestParts = listOf(), example = """ locale=pl&irrelevant=true """.trimIndent() @@ -1173,6 +1178,7 @@ class OpenApi20GeneratorTest { attributes = Attributes(enumValues = listOf("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE")) ) ), + requestParts = listOf(), example = getProductPayloadExample() ) } @@ -1197,7 +1203,8 @@ class OpenApi20GeneratorTest { ), queryParameters = listOf(), formParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ) } @@ -1213,7 +1220,8 @@ class OpenApi20GeneratorTest { pathParameters = listOf(), queryParameters = listOf(), formParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ) } @@ -1229,7 +1237,8 @@ class OpenApi20GeneratorTest { pathParameters = listOf(), queryParameters = listOf(), formParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ) } From 5139e90ada3aa8f0d5a1c584c1589610fb053f42 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 13:13:16 +0900 Subject: [PATCH 07/13] feat: Add FormData model --- .../apispec/postman/model/FormData.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/FormData.java diff --git a/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/FormData.java b/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/FormData.java new file mode 100644 index 00000000..16f1df74 --- /dev/null +++ b/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/FormData.java @@ -0,0 +1,121 @@ +package com.epages.restdocs.apispec.postman.model; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.util.List; + +/** + * FormData + *

+ * + * + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "key", + "type", + "src", + "description", + +}) +public class FormData { + /** + * + * (Required) + * + */ + @JsonProperty("key") + private String key; + /** + * A Type can be text, or be file + * + */ + @JsonProperty("type") + private String type; + /** + * The path to file, on the file system + * + */ + @JsonProperty("src") + private List src; + /** + * A Description can be a raw text, or be an object, which holds the description along with its format. + * + */ + @JsonProperty("description") + @JsonPropertyDescription("A Description can be a raw text, or be an object, which holds the description along with its format.") + private String description; + + /** + * + * (Required) + * + */ + @JsonProperty("key") + public String getKey() { + return key; + } + /** + * + * (Required) + * + */ + @JsonProperty("key") + public void setKey(String key) { + this.key = key; + } + + /** + * A Type can be text, or be file + * + */ + @JsonProperty("type") + public String getType() { + return type; + } + + /** + * A Type can be text, or be file + * + */ + @JsonProperty("type") + public void setType(String type) { + this.type = type; + } + /** + * The path to file, on the file system + * + */ + @JsonProperty("src") + public List getSrc() { + return src; + } + + /** + * The path to file, on the file system + * + */ + @JsonProperty("src") + public void setSrc(List src) { + this.src = src; + } + /** + * A Description can be a raw text, or be an object, which holds the description along with its format. + * + */ + @JsonProperty("description") + public String getDescription() { + return description; + } + /** + * A Description can be a raw text, or be an object, which holds the description along with its format. + * + */ + @JsonProperty("description") + public void setDescription(String description) { + this.description = description; + } +} From f817bfaee3c9950b50509bc82ca6b9ec53de2338 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 13:14:58 +0900 Subject: [PATCH 08/13] feat: Add formdata at Body --- .../epages/restdocs/apispec/postman/model/Body.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/Body.java b/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/Body.java index c73ad782..e3a690bf 100644 --- a/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/Body.java +++ b/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/Body.java @@ -32,6 +32,8 @@ public class Body { private String raw; @JsonProperty("urlencoded") private List urlencoded = new ArrayList(); + @JsonProperty("formdata") + private List formData = new ArrayList(); /** * Postman stores the type of data associated with this request in this field. @@ -71,6 +73,16 @@ public void setUrlencoded(List urlencoded) { this.urlencoded = urlencoded; } + @JsonProperty("formdata") + public List getFormData() { + return formData; + } + + @JsonProperty("formdata") + public void setFormData(List formData) { + this.formData = formData; + } + public enum Mode { RAW("raw"), From b756f8c052f3ab0de20f3b5c843d133b168b29fe Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 13:15:33 +0900 Subject: [PATCH 09/13] feat: Support PostmanCollectionGenerator requestParts --- .../postman/PostmanCollectionGenerator.kt | 35 +++++++++++++++---- .../postman/PostmanCollectionGeneratorTest.kt | 8 +++-- 2 files changed, 34 insertions(+), 9 deletions(-) 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 6406293a..4be8ccfa 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 @@ -1,9 +1,9 @@ package com.epages.restdocs.apispec.postman import com.epages.restdocs.apispec.model.HeaderDescriptor +import com.epages.restdocs.apispec.model.RequestPartFieldDescriptor import com.epages.restdocs.apispec.model.ResourceModel import com.epages.restdocs.apispec.model.groupByPath -import com.epages.restdocs.apispec.postman.model.Body import com.epages.restdocs.apispec.postman.model.Collection import com.epages.restdocs.apispec.postman.model.Header import com.epages.restdocs.apispec.postman.model.Info @@ -12,6 +12,8 @@ import com.epages.restdocs.apispec.postman.model.Query import com.epages.restdocs.apispec.postman.model.Request import com.epages.restdocs.apispec.postman.model.Response import com.epages.restdocs.apispec.postman.model.Src +import com.epages.restdocs.apispec.postman.model.Body +import com.epages.restdocs.apispec.postman.model.FormData import com.epages.restdocs.apispec.postman.model.Variable import java.net.URL @@ -66,12 +68,7 @@ object PostmanCollectionGenerator { return Request().apply { method = firstModel.request.method this.url = toUrl(modelsWithSamePathAndMethod, url) - body = firstModel.request.example?.let { - Body().apply { - raw = it - mode = Body.Mode.RAW - } - } + body = toBody(modelsWithSamePathAndMethod) header = modelsWithSamePathAndMethod .flatMap { it.request.headers } .distinctBy { it.name } @@ -123,6 +120,30 @@ object PostmanCollectionGenerator { } } + private fun toBody(modelsWithSamePathAndMethod: List): Body? { + val firstModel = modelsWithSamePathAndMethod.first() + return if (firstModel.request.contentType == "multipart/form-data") { + Body().apply { + mode = Body.Mode.FORMDATA + formData = firstModel.request.requestParts.map { + FormData().apply { + key = it.name + type = it.type + src = emptyList() + description = it.submittedFileName + } + } + } + } else { + firstModel.request.example?.let { + Body().apply { + raw = it + mode = Body.Mode.RAW + } + } + } + } + private fun List.toItemHeader(contentType: String?): List

{ return this.map { Header().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 9356dede..cd36939d 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 @@ -220,7 +220,8 @@ internal class PostmanCollectionGeneratorTest { queryParameters = listOf(), formParameters = listOf(), securityRequirements = null, - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ), response = ResponseModel( status = 204, @@ -337,6 +338,7 @@ internal class PostmanCollectionGeneratorTest { type = "STRING" ) ), + requestParts = listOf(), contentType = "application/json", example = """{ "description": "Good stuff!" @@ -370,6 +372,7 @@ internal class PostmanCollectionGeneratorTest { type = "STRING" ) ), + requestParts = listOf(), contentType = "application/json-patch+json", example = """ [ @@ -419,7 +422,8 @@ internal class PostmanCollectionGeneratorTest { ) ), formParameters = listOf(), - requestFields = listOf() + requestFields = listOf(), + requestParts = listOf() ) } From fe108367356d64a91a2e8f59012b6855caf3279e Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 14:51:31 +0900 Subject: [PATCH 10/13] test: Add Task Test --- .../apispec/gradle/ApiSpecTaskTest.kt | 57 +++++++++++++++++++ .../apispec/gradle/PostmanTaskTest.kt | 30 ++++++++++ .../gradle/RestdocsOpenApi3TaskTest.kt | 24 ++++++++ 3 files changed, 111 insertions(+) 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 ea8c2018..ecd1062f 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 @@ -237,4 +237,61 @@ abstract class ApiSpecTaskTest { protected fun givenBuildFileWithoutApiSpecClosure() { buildFile.writeText(baseBuildFile()) } + + fun givenResourceSnippetWithRequestParts() { + val operationDir = File(snippetsFolder, "some-operation").apply { mkdir() } + File(operationDir, "resource.json").writeText( + """ + { + "operationId" : "product-photo-upload", + "summary" : null, + "description" : null, + "privateResource" : false, + "deprecated" : false, + "request" : { + "path" : "/products/photo/{id}", + "method" : "POST", + "contentType" : "multipart/form-data", + "headers" : [ { + "name" : "one", + "attributes" : { }, + "description" : "Override request header param", + "type" : "STRING", + "optional" : true, + "example" : "one", + "default" : "a default value" + } ], + "pathParameters" : [ ], + "queryParameters" : [ ], + "formParameters" : [ ], + "requestFields" : [ ], + "requestParts" : [ + { + "content" : "dGVzdA==", + "headers" : { + "Content-Type" : [ "image/jpeg" ], + "Content-Length" : [ "123" ] + }, + "name" : "photo", + "submittedFileName" : "photo.jpg", + "contentAsString" : "photo" + } + ], + "example" : null, + "securityRequirements" : { + "type": "OAUTH2", + "requiredScopes": ["prod:r"] + } + }, + "response" : { + "status" : 200, + "contentType" : "application/hal+json", + "headers" : [ ], + "responseFields" : [ ], + "example" : "{\n \"name\" : \"Fancy pants\",\n \"price\" : 49.99,\n \"_links\" : {\n \"self\" : {\n \"href\" : \"http://localhost:8080/products/7\"\n },\n \"product\" : {\n \"href\" : \"http://localhost:8080/products/7\"\n }\n }\n}" + } +} + """.trimIndent() + ) + } } diff --git a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/PostmanTaskTest.kt b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/PostmanTaskTest.kt index 2a1cf85f..dc694b93 100644 --- a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/PostmanTaskTest.kt +++ b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/PostmanTaskTest.kt @@ -63,6 +63,36 @@ class PostmanTaskTest : ApiSpecTaskTest() { thenOutputFileForPublicResourceSpecificationFound() } + @Test + fun `should run postman task with request parts`() { + title = "my custom title" + version = "2.0.0" + baseUrl = "https://example.com:8080/api" + + givenBuildFileWithPostmanClosure() + givenResourceSnippetWithRequestParts() + + whenPluginExecuted() + + thenApiSpecTaskSuccessful() + thenOutputFileFound() + thenOutputFileForPublicResourceSpecificationNotFound() + + with(outputFileContext()) { + then(read("info.name")).isEqualTo(title) + then(read("info.version")).isEqualTo(version) + then(read("item[0].request.url.protocol")).isEqualTo("https") + then(read("item[0].request.url.host")).isEqualTo("example.com") + then(read("item[0].request.url.port")).isEqualTo("8080") + then(read("item[0].request.url.path")).isEqualTo("/api/products/photo/:id") + then(read("item[0].request.header[1].value")).isEqualTo("multipart/form-data") + then(read("item[0].request.body.mode")).isEqualTo("formdata") + then(read("item[0].request.body.formdata[0].key")).isEqualTo("photo") + then(read("item[0].request.body.formdata[0].type")).isEqualTo("file") + then(read>("item[0].request.body.formdata[0].src")).isEmpty() + } + } + private fun givenBuildFileWithPostmanClosure() { buildFile.writeText( baseBuildFile() + """ diff --git a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApi3TaskTest.kt b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApi3TaskTest.kt index fa7ef77d..b7f18bb7 100644 --- a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApi3TaskTest.kt +++ b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApi3TaskTest.kt @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junitpioneer.jupiter.TempDirectory import java.lang.Boolean.FALSE +import java.util.LinkedList @ExtendWith(TempDirectory::class) class RestdocsOpenApi3TaskTest : RestdocsOpenApiTaskTestBase() { @@ -73,6 +74,18 @@ class RestdocsOpenApi3TaskTest : RestdocsOpenApiTaskTestBase() { thenHeaderWithDefaultValuesContainedInOutput() } + @Test + fun `should run openapi task with request parts`() { + givenBuildFileWithOpenApiClosureWithSingleServerString() + givenResourceSnippetWithRequestParts() + + whenPluginExecuted() + + thenApiSpecTaskSuccessful() + thenOutputFileFound() + thenRequestPartsContainedInOutput() + } + private fun thenSingleServerContainedInOutput() { with(outputFileContext()) { then(read>("servers[*].url")).containsOnly("http://some.api") @@ -96,6 +109,17 @@ class RestdocsOpenApi3TaskTest : RestdocsOpenApiTaskTestBase() { } } + private fun thenRequestPartsContainedInOutput() { + with(outputFileContext()) { + then(read>("paths./products/photo/{id}.post.requestBody.content.multipart/form-data.schema")).isNotEmpty() + val multipartSchemaKey = read>("components.schemas").keys.toList()[0] + then(read("components.schemas.${multipartSchemaKey}.required[0]")).isEqualTo("photo") + then(read("components.schemas.${multipartSchemaKey}.type")).isEqualTo("object") + then(read("components.schemas.${multipartSchemaKey}.properties.photo.type")).isEqualTo("string") + then(read("components.schemas.${multipartSchemaKey}.properties.photo.format")).isEqualTo("binary") + } + } + fun givenBuildFileWithOpenApiClosureWithSingleServerString() { givenBuildFileWithOpenApiClosure("server", """ 'http://some.api' """) } From 48d6f83f11c93157adf674eb865e98c5b2145024 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 14:52:07 +0900 Subject: [PATCH 11/13] refactor: Change FileSchema's parent Class to StringSchema --- .../restdocs/apispec/jsonschema/schema/FileSchema.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/schema/FileSchema.kt b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/schema/FileSchema.kt index a4e00bd5..aefe6b9b 100644 --- a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/schema/FileSchema.kt +++ b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/schema/FileSchema.kt @@ -1,14 +1,15 @@ package com.epages.restdocs.apispec.jsonschema.schema -import org.everit.json.schema.EmptySchema +import org.everit.json.schema.StringSchema import org.everit.json.schema.internal.JSONPrinter class FileSchema( builder: BinarySchemaBuilder -) : EmptySchema(builder) { +) : StringSchema(builder) { val format: String = builder.format - class BinarySchemaBuilder : EmptySchema.Builder() { + + class BinarySchemaBuilder : StringSchema.Builder() { internal var format: String = "binary" From b1bd1cc302592a599a70084b427d472f44e2c807 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 14:52:38 +0900 Subject: [PATCH 12/13] test: Add should_generate_schema_for_file_values test --- ...SchemaFromFieldDescriptorsGeneratorTest.kt | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt index a9ed3b12..7332f0d6 100644 --- a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt +++ b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt @@ -1,5 +1,6 @@ package com.epages.restdocs.apispec.jsonschema +import com.epages.restdocs.apispec.jsonschema.schema.FileSchema import com.epages.restdocs.apispec.model.Attributes import com.epages.restdocs.apispec.model.Constraint import com.epages.restdocs.apispec.model.FieldDescriptor @@ -453,6 +454,31 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { thenSchemaIsValid() } + @Test + fun should_generate_schema_for_file_values() { + givenFieldDescriptorWithFile() + + whenSchemaGenerated() + + then(schema).isInstanceOf(ObjectSchema::class.java) + val fileSchemaSource = (schema as ObjectSchema).propertySchemas["some"]!! + val fileSchema = FileSchema.builder() + .title(fileSchemaSource.title) + .description(fileSchemaSource.description) + .schemaLocation(fileSchemaSource.schemaLocation) + .nullable(fileSchemaSource.isNullable) + .readOnly(fileSchemaSource.isReadOnly) + .writeOnly(fileSchemaSource.isWriteOnly) + .unprocessedProperties(fileSchemaSource.unprocessedProperties) + .build() + + then(fileSchema).isInstanceOf(FileSchema::class.java) + fileSchema as FileSchema + then(fileSchema.format).isEqualTo("binary") + + thenSchemaIsValid() + } + private fun thenSchemaIsValid() { val report = JsonSchemaFactory.byDefault() @@ -822,8 +848,18 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { ) } + private fun givenFieldDescriptorWithFile() { + fieldDescriptors = listOf(FieldDescriptor("some","", "file")) + } + private fun thenSchemaValidatesJson(json: String) { - schema!!.validate(if (json.startsWith("[")) JSONArray(json) else JSONObject(json)) + schema!!.validate( + if (json.startsWith("[")) { + JSONArray(json) + } else { + JSONObject(json) + } + ) } private fun thenSchemaDoesNotValidateJson(json: String) { From 091b85bdc85eaf998bb65d8717eb69db2421e53f Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Sat, 7 Sep 2024 15:42:37 +0900 Subject: [PATCH 13/13] feat: add formdata property order --- .../java/com/epages/restdocs/apispec/postman/model/Body.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/Body.java b/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/Body.java index e3a690bf..1d15874a 100644 --- a/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/Body.java +++ b/restdocs-api-spec-postman-generator/src/main/java/com/epages/restdocs/apispec/postman/model/Body.java @@ -17,7 +17,8 @@ @JsonPropertyOrder({ "mode", "raw", - "urlencoded" + "urlencoded", + "formdata" }) public class Body {