From 94c658f9ada5625964ac8949006b0a395c1b0b68 Mon Sep 17 00:00:00 2001 From: Lenard Gaida Date: Fri, 17 Jan 2020 14:34:25 +0100 Subject: [PATCH 1/6] Add test for xml based request and response --- restdocs-api-spec-mockmvc/build.gradle.kts | 1 + ...RestDocumentationWrapperIntegrationTest.kt | 43 ++++++- .../apispec/ResourceSnippetIntegrationTest.kt | 114 ++++++++++++++++-- 3 files changed, 142 insertions(+), 16 deletions(-) diff --git a/restdocs-api-spec-mockmvc/build.gradle.kts b/restdocs-api-spec-mockmvc/build.gradle.kts index 85575a87..5e365bd8 100644 --- a/restdocs-api-spec-mockmvc/build.gradle.kts +++ b/restdocs-api-spec-mockmvc/build.gradle.kts @@ -22,5 +22,6 @@ dependencies { testCompile("org.junit.jupiter:junit-jupiter-engine:$junitVersion") testImplementation("org.junit-pioneer:junit-pioneer:0.3.0") testCompile("org.springframework.boot:spring-boot-starter-hateoas:$springBootVersion") + testCompile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.10.0") } diff --git a/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt b/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt index 9ecbe5cc..1c5b73e3 100644 --- a/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt +++ b/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt @@ -9,11 +9,15 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.hateoas.MediaTypes.HAL_JSON import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.http.MediaType.APPLICATION_XML import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName import org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders import org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders import org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel import org.springframework.restdocs.hypermedia.HypermediaDocumentation.links +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import org.springframework.restdocs.payload.PayloadDocumentation.requestFields @@ -24,8 +28,6 @@ import org.springframework.restdocs.request.RequestDocumentation.pathParameters import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print -import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document -import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.io.File @@ -103,6 +105,15 @@ class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired private val mock thenSnippetFileExists() } + @Test + fun should_document_request_with_fields_when_fields_are_xml() { + givenEndpointInvokedWithXml() + + whenResourceSnippetDocumentedWithRequestAndResponseFieldsWithXml() + + thenSnippetFileExists() + } + @Test fun should_document_request_with_null_field() { givenEndpointInvoked("null") @@ -126,6 +137,11 @@ class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired private val mock .andDo(document(operationName, buildFullResourceSnippet())) } + private fun whenResourceSnippetDocumentedWithRequestAndResponseFieldsWithXml() { + resultActions + .andDo(document(operationName, buildFullResourceSnippetWithXml())) // XML + } + private fun givenEndpointInvoked(flagValue: String = "true") { resultActions = mockMvc.perform( post("/some/{someId}/other/{otherId}", "id", 1) @@ -137,8 +153,27 @@ class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired private val mock "flag": $flagValue, "count": 1 }""".trimIndent() - ) - ).andExpect(status().isOk) + ) + ).andExpect(status().isOk) + } + + private fun givenEndpointInvokedWithXml(flagValue: String = "true") { + resultActions = mockMvc.perform( + put("/some/{someId}/other/{otherId}", "id", 1) + .contentType(APPLICATION_XML) + .header("X-Custom-Header", "test") + .accept(APPLICATION_XML) + .content(""" + + + some + $flagValue + 1 + + """.trimIndent() + ) + .characterEncoding("UTF-8") + ).andExpect(status().isOk) } private fun thenSnippetFileExists() { 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 2e73b22a..049688bd 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 @@ -14,18 +14,21 @@ import org.springframework.hateoas.Resource import org.springframework.hateoas.mvc.BasicLinkBuilder import org.springframework.http.HttpHeaders.ACCEPT import org.springframework.http.HttpHeaders.CONTENT_TYPE +import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName import org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel +import org.springframework.restdocs.payload.JsonFieldType import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.ResultActions import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping 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 java.util.* import javax.validation.constraints.NotEmpty @ExtendWith(SpringExtension::class) @@ -49,13 +52,36 @@ open class ResourceSnippetIntegrationTest { @PostMapping(path = ["/some/{someId}/other/{otherId}"]) fun doSomething( - @PathVariable someId: String, - @PathVariable otherId: Int?, - @RequestHeader("X-Custom-Header") customHeader: String, - @RequestBody testDataHolder: TestDataHolder - ): ResponseEntity> { + @PathVariable someId: String, + @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() + 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")) + + return ResponseEntity + .ok() + .header("X-Custom-Header", customHeader) + .body>(resource) + } + + @PutMapping(path = ["/some/{someId}/other/{otherId}"], + consumes = [MediaType.APPLICATION_XML_VALUE], + produces = [MediaType.APPLICATION_XML_VALUE]) + fun doSomethingWithXml( + @PathVariable someId: String, + @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")) @@ -99,19 +125,83 @@ fun buildFullResourceSnippet(): ResourceSnippet { .requestHeaders( headerWithName("X-Custom-Header").description("A custom header"), headerWithName(ACCEPT).description("Accept") - ) + ) .responseHeaders( headerWithName("X-Custom-Header").description("A custom header"), headerWithName(CONTENT_TYPE).description("ContentType") - ) + ) .pathParameters( parameterWithName("someId").description("some id"), parameterWithName("otherId").description("otherId id").type(SimpleType.INTEGER) - ) + ) .links( linkWithRel("self").description("some"), linkWithRel("multiple").description("multiple") - ) + ) .build() - ) + ) +} + + +fun fieldDescriptorsForXmlRequest(): FieldDescriptors { + val fields = ConstrainedFields(ResourceSnippetIntegrationTest.TestDataHolder::class.java) + return ResourceDocumentation.fields( + fields.withPath("testDataHolder").type(JsonFieldType.OBJECT).description("the data holder").optional(), + fields.withPath("testDataHolder/comment").type(JsonFieldType.STRING).description("the comment").optional(), + fields.withPath("testDataHolder/flag").type(JsonFieldType.BOOLEAN).description("the flag"), + fields.withPath("testDataHolder/count").type(JsonFieldType.NUMBER).description("the count") + ) +} + + +fun fieldDescriptorsForXmlResponse(): FieldDescriptors { + val fields = ConstrainedFields(ResourceSnippetIntegrationTest.TestDataHolder::class.java) + return ResourceDocumentation.fields( + fields.withPath("Resource").type(JsonFieldType.OBJECT).description("the data holder").optional(), + fields.withPath("Resource/comment").type(JsonFieldType.STRING).description("the comment").optional(), + fields.withPath("Resource/flag").type(JsonFieldType.BOOLEAN).description("the flag"), + fields.withPath("Resource/count").type(JsonFieldType.NUMBER).description("the count"), + fields.withPath("Resource/id").type(JsonFieldType.STRING).description("id"), + // incorporate links here, see buildFullResourceSnippetWithXml() + fields.withPath("Resource/links").type(JsonFieldType.ARRAY).description("array of links"), + fields.withPath("Resource/links/links").type(JsonFieldType.OBJECT).description("link object"), + fields.withPath("Resource/links/links/rel").type(JsonFieldType.STRING).description("rel of link"), + fields.withPath("Resource/links/links/href").type(JsonFieldType.STRING).description("href of link"), + fields.withPath("Resource/links/links/hreflang").type(JsonFieldType.STRING).description("hreflang of link"), + fields.withPath("Resource/links/links/media").type(JsonFieldType.STRING).description("media of link"), + fields.withPath("Resource/links/links/title").type(JsonFieldType.STRING).description("title of link"), + fields.withPath("Resource/links/links/type").type(JsonFieldType.STRING).description("type of link"), + fields.withPath("Resource/links/links/deprecation").type(JsonFieldType.STRING).description("deprecation of link") + ) +} + +fun buildFullResourceSnippetWithXml(): ResourceSnippet { + return resource( + ResourceSnippetParameters.builder() + .description("description") + .summary("summary") + .deprecated(true) + .privateResource(true) + .requestFields(fieldDescriptorsForXmlRequest()) + .responseFields(fieldDescriptorsForXmlResponse()) + .requestHeaders( + headerWithName("X-Custom-Header").description("A custom header"), + headerWithName(ACCEPT).description("Accept") + ) + .responseHeaders( + headerWithName("X-Custom-Header").description("A custom header"), + headerWithName(CONTENT_TYPE).description("ContentType") + ) + .pathParameters( + parameterWithName("someId").description("some id"), + parameterWithName("otherId").description("otherId id").type(SimpleType.INTEGER) + ) + /* + Can not be used, since spring framework expects links to be json, if we try this with xml it will fail + .links( + linkWithRel("self").description("some"), + linkWithRel("multiple").description("multiple") + )*/ + .build() + ) } From 4e6410cc19f04d1ea0ff6f360b52c484ae68764d Mon Sep 17 00:00:00 2001 From: Lenard Gaida Date: Fri, 17 Jan 2020 14:35:58 +0100 Subject: [PATCH 2/6] Add slash delimiter for property name resolving in ConstrainedFields --- .../restdocs/apispec/ConstrainedFields.kt | 26 ++++++---- .../restdocs/apispec/ConstrainedFieldsTest.kt | 51 +++++++++++++++---- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ConstrainedFields.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ConstrainedFields.kt index 224932c4..c1c9d4b1 100644 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ConstrainedFields.kt +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ConstrainedFields.kt @@ -15,7 +15,7 @@ class ConstrainedFields(private val classHoldingConstraints: Class<*>) { /** * Create a field description with constraints for bean property with the same name - * @param path json path of the field + * @param path json path or xpath of the field */ fun withPath(path: String): FieldDescriptor = withMappedPath(path, beanPropertyNameFromPath(path)) @@ -23,25 +23,31 @@ class ConstrainedFields(private val classHoldingConstraints: Class<*>) { /** * * Create a field description with constraints for bean property with a name differing from the path - * @param jsonPath json path of the field + * @param path json path or xpath of the field * @param beanPropertyName name of the property of the bean that is used to get the field constraints */ - fun withMappedPath(jsonPath: String, beanPropertyName: String): FieldDescriptor = - addConstraints(fieldWithPath(jsonPath), beanPropertyName) + fun withMappedPath(path: String, beanPropertyName: String): FieldDescriptor = + addConstraints(fieldWithPath(path), beanPropertyName) /** * Add bean validation constraints for the field beanPropertyName to the descriptor */ fun addConstraints(fieldDescriptor: FieldDescriptor, beanPropertyName: String): FieldDescriptor = - fieldDescriptor.attributes( - key(CONSTRAINTS_KEY) - .value(this.validatorConstraintResolver.resolveForProperty(beanPropertyName, classHoldingConstraints)) - ) - - private fun beanPropertyNameFromPath(jsonPath: String) = jsonPath.substringAfterLast(DOT_NOTATION_DELIMITER) + fieldDescriptor.attributes( + key(CONSTRAINTS_KEY) + .value(this.validatorConstraintResolver.resolveForProperty(beanPropertyName, + classHoldingConstraints)) + ) + + private fun beanPropertyNameFromPath(path: String) = + if (path.contains(SLASH_NOTATION_DELIMITER)) + path.substringAfterLast(SLASH_NOTATION_DELIMITER) + else + path.substringAfterLast(DOT_NOTATION_DELIMITER) companion object { private const val CONSTRAINTS_KEY = "validationConstraints" private const val DOT_NOTATION_DELIMITER = "." + private const val SLASH_NOTATION_DELIMITER = "/" } } 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 00f3ca09..7585b8a1 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 @@ -9,29 +9,29 @@ internal class ConstrainedFieldsTest { @Test @Suppress("UNCHECKED_CAST") - fun `should resolve constraints`() { + fun `should resolve json constraints`() { val fields = ConstrainedFields(SomeWithConstraints::class.java) val descriptor = fields.withPath("nonEmpty") then(descriptor.attributes).containsKey("validationConstraints") then((descriptor.attributes["validationConstraints"] as List).map { it.name }) - .containsExactly(NotEmpty::class.java.name) + .containsExactly(NotEmpty::class.java.name) } @Test @Suppress("UNCHECKED_CAST") - fun `should resolve one level nested constraints`() { + fun `should resolve one level nested json constraints`() { val fields = ConstrainedFields(SomeWithConstraints::class.java) val descriptor = fields.withPath("nested.nonEmpty") then(descriptor.attributes).containsKey("validationConstraints") then((descriptor.attributes["validationConstraints"] as List).map { it.name }) - .containsExactly(NotEmpty::class.java.name) + .containsExactly(NotEmpty::class.java.name) } @Test @Suppress("UNCHECKED_CAST") - fun `should resolve two level nested constraints`() { + fun `should resolve two level nested json constraints`() { val fields = ConstrainedFields(SomeWithConstraints::class.java) val descriptor = fields.withPath("nested.nested.nonEmpty") @@ -40,10 +40,43 @@ internal class ConstrainedFieldsTest { .containsExactly(NotEmpty::class.java.name) } + @Test + @Suppress("UNCHECKED_CAST") + fun `should resolve xml constraints`() { + val fields = ConstrainedFields(SomeWithConstraints::class.java) + val descriptor = fields.withPath("nonEmpty") + + then(descriptor.attributes).containsKey("validationConstraints") + then((descriptor.attributes["validationConstraints"] as List).map { it.name }) + .containsExactly(NotEmpty::class.java.name) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `should resolve one level nested xml constraints`() { + val fields = ConstrainedFields(SomeWithConstraints::class.java) + val descriptor = fields.withPath("nested/nonEmpty") + + then(descriptor.attributes).containsKey("validationConstraints") + then((descriptor.attributes["validationConstraints"] as List).map { it.name }) + .containsExactly(NotEmpty::class.java.name) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun `should resolve two level nested xml constraints`() { + val fields = ConstrainedFields(SomeWithConstraints::class.java) + val descriptor = fields.withPath("nested/nested/nonEmpty") + + then(descriptor.attributes).containsKey("validationConstraints") + then((descriptor.attributes["validationConstraints"] as List).map { it.name }) + .containsExactly(NotEmpty::class.java.name) + } + private data class SomeWithConstraints( - @field:NotEmpty - val nonEmpty: String, + @field:NotEmpty + val nonEmpty: String, - val nested: SomeWithConstraints? - ) + val nested: SomeWithConstraints? + ) } From 531efbef223a5fb84d32089b18965ead8f64e35e Mon Sep 17 00:00:00 2001 From: Lenard Gaida Date: Fri, 17 Jan 2020 14:37:59 +0100 Subject: [PATCH 3/6] Watch out for xpath in field descriptors when generating json schema --- ...JsonSchemaFromFieldDescriptorsGenerator.kt | 47 +++- ...SchemaFromFieldDescriptorsGeneratorTest.kt | 43 +++- .../apispec/ResourceSnippetIntegrationTest.kt | 2 - .../apispec/openapi3/OpenApi3Generator.kt | 11 +- .../apispec/openapi3/OpenApi3GeneratorTest.kt | 210 ++++++++++++++---- .../restdocs/apispec/ConstrainedFields.kt | 10 +- .../restdocs/apispec/ConstrainedFieldsTest.kt | 1 - 7 files changed, 243 insertions(+), 81 deletions(-) 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 8d584f31..2a4e3350 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 @@ -18,21 +18,56 @@ import org.everit.json.schema.Schema import org.everit.json.schema.StringSchema import org.everit.json.schema.internal.JSONPrinter import java.io.StringWriter -import java.util.ArrayList +import java.util.* import java.util.Collections.emptyList import java.util.function.Predicate class JsonSchemaFromFieldDescriptorsGenerator { fun generateSchema(fieldDescriptors: List, title: String? = null): String { - val jsonFieldPaths = reduceFieldDescriptors(fieldDescriptors) - .map { JsonFieldPath.compile(it) } + var workingFieldDescriptors = fieldDescriptors - val schema = traverse(emptyList(), jsonFieldPaths, ObjectSchema.builder().title(title) as ObjectSchema.Builder) + var rootElementName = "" + if (fieldDescriptors.any { fieldDescriptor -> fieldDescriptor.path.contains('/') }) { + rootElementName = findRootElementName(fieldDescriptors) + workingFieldDescriptors = replaceRootElementAndSlashes(fieldDescriptors, rootElementName) + } + + val jsonFieldPaths = reduceFieldDescriptors(workingFieldDescriptors) + .map { JsonFieldPath.compile(it) } + + val schemaTitle = if (title.isNullOrEmpty()) rootElementName else title + val schema = traverse(emptyList(), + jsonFieldPaths, + ObjectSchema.builder().title(schemaTitle) as ObjectSchema.Builder) return toFormattedString(unWrapRootArray(jsonFieldPaths, schema)) } + private fun findRootElementName(fieldDescriptors: List): String { + val distinctStarters = fieldDescriptors.map { fieldDescriptor -> fieldDescriptor.path.split('/').first() }.distinct() + if (distinctStarters.size == 1) { + return distinctStarters.first() + } + return "" + } + + private fun replaceRootElementAndSlashes(fieldDescriptors: List, + rootElementName: String): List { + return fieldDescriptors + .filter { fieldDescriptor -> fieldDescriptor.path != rootElementName } + .map { fieldDescriptor -> + FieldDescriptor( + fieldDescriptor.path.replace(rootElementName + "/", "").replace('/', '.'), + fieldDescriptor.description, + fieldDescriptor.type, + fieldDescriptor.optional, + fieldDescriptor.ignored, + fieldDescriptor.attributes + ) + } + } + /** * Reduce the list of field descriptors so that the path of each list item is unique. * @@ -40,8 +75,8 @@ class JsonSchemaFromFieldDescriptorsGenerator { */ private fun reduceFieldDescriptors(fieldDescriptors: List): List { return fieldDescriptors - .map { - FieldDescriptorWithSchemaType.fromFieldDescriptor( + .map { + FieldDescriptorWithSchemaType.fromFieldDescriptor( it ) } 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 439f1d9a..2810618a 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 @@ -120,6 +120,18 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { thenSchemaValidatesJson("""[{"id": "some"}]""") } + @Test + fun should_generate_schema_for_xpath_descriptors() { + givenFieldDescriptorWithXpathDescriptors() + + whenSchemaGenerated() + + then(schema).isInstanceOf(ObjectSchema::class.java) + thenSchemaIsValid() + then(schema?.title).isEqualTo("root") + thenSchemaValidatesJson("""{"a": {"suba":"some suba"}, "b": "some b"}""") + } + @Test fun should_generate_schema_for_top_level_array_of_any() { givenFieldDescriptorWithTopLevelArrayOfAny() @@ -241,21 +253,30 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { private fun givenDifferentFieldDescriptorsWithSamePathAndDifferentTypes() { fieldDescriptors = listOf( - FieldDescriptor("id", "some", "STRING"), - FieldDescriptor("id", "some", "NULL"), - FieldDescriptor("id", "some", "BOOLEAN") - ) + FieldDescriptor("id", "some", "STRING"), + FieldDescriptor("id", "some", "NULL"), + FieldDescriptor("id", "some", "BOOLEAN") + ) + } + + private fun givenFieldDescriptorWithXpathDescriptors() { + fieldDescriptors = listOf( + FieldDescriptor("root", "some root", "OBJECT"), + FieldDescriptor("root/a", "some a", "OBJECT"), + FieldDescriptor("root/a/suba", "some sub a", "STRING"), + FieldDescriptor("root/b", "some b", "STRING") + ) } private fun givenFieldDescriptorsWithConstraints() { val constraintAttributeWithNotNull = - Attributes( - listOf( - Constraint( - NotNull::class.java.name, - emptyMap() - ) - ) + Attributes( + listOf( + Constraint( + NotNull::class.java.name, + emptyMap() + ) + ) ) val constraintAttributeWithLength = 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 049688bd..41177ce5 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 @@ -142,7 +142,6 @@ fun buildFullResourceSnippet(): ResourceSnippet { ) } - fun fieldDescriptorsForXmlRequest(): FieldDescriptors { val fields = ConstrainedFields(ResourceSnippetIntegrationTest.TestDataHolder::class.java) return ResourceDocumentation.fields( @@ -153,7 +152,6 @@ fun fieldDescriptorsForXmlRequest(): FieldDescriptors { ) } - fun fieldDescriptorsForXmlResponse(): FieldDescriptors { val fields = ConstrainedFields(ResourceSnippetIntegrationTest.TestDataHolder::class.java) return ResourceDocumentation.fields( 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 cc9a0714..2a5fe46a 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 @@ -51,7 +51,6 @@ object OpenApi3Generator { oauth2SecuritySchemeDefinition: Oauth2Configuration? = null ): OpenAPI { return OpenAPI().apply { - this.servers = servers info = Info().apply { this.title = title @@ -128,10 +127,10 @@ object OpenApi3Generator { .forEach { it.schema( extractOrFindSchema( - schemasToKeys, - it.schema, - generateSchemaName(path) - ) + schemasToKeys, + it.schema, + generateSchemaName(path) + ) ) } } @@ -140,7 +139,7 @@ object OpenApi3Generator { val schemaKey = if (schemasToKeys.containsKey(schema)) { schemasToKeys[schema]!! } else { - val name = schemaNameGenerator(schema) + val name = if (!schema.title.isNullOrEmpty()) schema.title else schemaNameGenerator(schema) schemasToKeys[schema] = name 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 d80c6918..d5622a7e 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 @@ -37,6 +37,22 @@ class OpenApi3GeneratorTest { thenInfoFieldsPresent() thenTagFieldsPresent() thenServersPresent() + thenSchemaPresentNamedLike("product") + thenOpenApiSpecIsValid() + } + + @Test + fun `should convert single resource model to openapi with xml in response body`() { + givenGetProductResourceModelWithXmlResponse() + + whenOpenApiObjectGenerated() + + thenGetProductByIdOperationIsValidWithXml() + thenOAuth2SecuritySchemesPresent() + thenInfoFieldsPresent() + thenTagFieldsPresent() + thenServersPresent() + thenSchemaPresentNamedLike("TestDataHolder") thenOpenApiSpecIsValid() } @@ -160,8 +176,10 @@ class OpenApi3GeneratorTest { then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'locale')].schema.type")).containsOnly("string") then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].in")).containsOnly("header") then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].required")).containsOnly(true) - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].example")).containsOnly("some example") - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].schema.type")).containsOnly("string") + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].example")).containsOnly( + "some example") + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].schema.type")).containsOnly( + "string") then(openApiJsonPathContext.read("$productGetByIdPath.requestBody")).isNull() @@ -170,8 +188,50 @@ class OpenApi3GeneratorTest { then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/json.schema.\$ref")).isNotNull() then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/json.examples.test.value")).isNotNull() - then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_clientCredentials").flatMap { it }).containsOnly("prod:r") - then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_authorizationCode").flatMap { it }).containsOnly("prod:r") + then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_clientCredentials").flatMap { it }).containsOnly( + "prod:r") + then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_authorizationCode").flatMap { it }).containsOnly( + "prod:r") + } + + fun thenGetProductByIdOperationIsValidWithXml() { + val productGetByIdPath = "paths./products/{id}.get" + then(openApiJsonPathContext.read>("$productGetByIdPath.tags")).isNotNull() + then(openApiJsonPathContext.read("$productGetByIdPath.operationId")).isNotNull() + then(openApiJsonPathContext.read("$productGetByIdPath.summary")).isNotNull() + then(openApiJsonPathContext.read("$productGetByIdPath.description")).isNotNull() + then(openApiJsonPathContext.read("$productGetByIdPath.deprecated")).isNull() + + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'id')].in")).containsOnly( + "path") + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'id')].required")).containsOnly( + true) + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'locale')].in")).containsOnly( + "query") + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'locale')].required")).containsOnly( + false) + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'locale')].schema.type")).containsOnly( + "string") + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].in")).containsOnly( + "header") + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].required")).containsOnly( + true) + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].example")).containsOnly( + "some example") + then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].schema.type")).containsOnly( + "string") + + then(openApiJsonPathContext.read("$productGetByIdPath.requestBody")).isNull() + + then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.description")).isNotNull() + then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.headers.SIGNATURE.schema.type")).isNotNull() + then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/xml.schema.\$ref")).isNotNull() + then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/xml.examples.test.value")).isNotNull() + + then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_clientCredentials").flatMap { it }).containsOnly( + "prod:r") + then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_authorizationCode").flatMap { it }).containsOnly( + "prod:r") } private fun thenServersPresent() { @@ -191,14 +251,23 @@ class OpenApi3GeneratorTest { then(openApiJsonPathContext.read("tags[1].description")).isEqualTo("tag2 description") } + private fun thenSchemaPresentNamedLike(desiredNamepart: String) { + val schemas = openApiJsonPathContext.read>("components.schemas") + then(schemas).isNotEmpty() + val schemakey = schemas.filterKeys { key -> key.contains(desiredNamepart) }.keys + then(schemakey).hasSize(1) + then(openApiJsonPathContext.read>("components.schemas." + schemakey.first() + ".properties")).hasSize( + 2) + } + private fun thenOAuth2SecuritySchemesPresent() { then(openApiJsonPathContext.read("components.securitySchemes.oauth2.type")).isEqualTo("oauth2") then(openApiJsonPathContext.read>("components.securitySchemes.oauth2.flows")) - .containsKeys("clientCredentials", "authorizationCode") + .containsKeys("clientCredentials", "authorizationCode") then(openApiJsonPathContext.read>("components.securitySchemes.oauth2.flows.clientCredentials.scopes")) - .containsKeys("prod:r") + .containsKeys("prod:r") then(openApiJsonPathContext.read>("components.securitySchemes.oauth2.flows.authorizationCode.scopes")) - .containsKeys("prod:r") + .containsKeys("prod:r") } private fun thenJWTSecuritySchemesPresent() { @@ -414,17 +483,32 @@ class OpenApi3GeneratorTest { private fun givenGetProductResourceModel() { resources = listOf( - ResourceModel( - operationId = "test", - summary = "summary", - description = "description", - privateResource = false, - deprecated = false, - tags = setOf("tag1", "tag2"), - request = getProductRequest(), - response = getProductResponse() - ) - ) + ResourceModel( + operationId = "test", + summary = "summary", + description = "description", + privateResource = false, + deprecated = false, + tags = setOf("tag1", "tag2"), + request = getProductRequest(), + response = getProductResponse() + ) + ) + } + + private fun givenGetProductResourceModelWithXmlResponse() { + resources = listOf( + ResourceModel( + operationId = "test", + summary = "summary", + description = "description", + privateResource = false, + deprecated = false, + tags = setOf("tag1", "tag2"), + request = getProductRequest(), + response = getProductXmlResponse() + ) + ) } private fun givenGetProductResourceModelWithJWTSecurityRequirement() { @@ -462,44 +546,72 @@ class OpenApi3GeneratorTest { private fun getProductResponse(): ResponseModel { return ResponseModel( - status = 200, - contentType = "application/json", - headers = listOf( - HeaderDescriptor( - name = "SIGNATURE", - description = "This is some signature", - type = "STRING", - optional = false - ) - ), - responseFields = listOf( - FieldDescriptor( - path = "_id", - description = "ID of the product", - type = "STRING" - ), - FieldDescriptor( - path = "description", - description = "Product description, localized.", - type = "STRING" - ) - ), - example = """{ + status = 200, + contentType = "application/json", + headers = listOf( + HeaderDescriptor( + name = "SIGNATURE", + description = "This is some signature", + type = "STRING", + optional = false + ) + ), + responseFields = listOf( + FieldDescriptor( + path = "_id", + description = "ID of the product", + type = "STRING" + ), + FieldDescriptor( + path = "description", + description = "Product description, localized.", + type = "STRING" + ) + ), + example = """{ "_id": "123", "description": "Good stuff!" }""" - ) + ) + } + + private fun getProductXmlResponse(): ResponseModel { + return ResponseModel( + status = 200, + contentType = "application/xml", + headers = listOf( + HeaderDescriptor( + name = "SIGNATURE", + description = "This is some signature", + type = "STRING", + optional = false + ) + ), + responseFields = listOf( + FieldDescriptor( + path = "TestDataHolder/_id", + description = "ID of the product", + type = "STRING" + ), + FieldDescriptor( + path = "TestDataHolder/description", + description = "Product description, localized.", + type = "STRING" + ) + ), + example = """<_id>123Good stuff!""".trimMargin() + ) } private fun getProductHalResponse(): ResponseModel { return ResponseModel( - status = 200, - contentType = "application/hal+json", - responseFields = listOf( - FieldDescriptor( - path = "_id", - description = "ID of the product", - type = "STRING" + status = 200, + contentType = "application/hal+json", + responseFields = listOf( + FieldDescriptor( + path = "_id", + description = "ID of the product", + type = "STRING" ), FieldDescriptor( path = "description1", diff --git a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ConstrainedFields.kt b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ConstrainedFields.kt index c1c9d4b1..3bd27d33 100644 --- a/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ConstrainedFields.kt +++ b/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ConstrainedFields.kt @@ -18,7 +18,7 @@ class ConstrainedFields(private val classHoldingConstraints: Class<*>) { * @param path json path or xpath of the field */ fun withPath(path: String): FieldDescriptor = - withMappedPath(path, beanPropertyNameFromPath(path)) + withMappedPath(path, beanPropertyNameFromPath(path)) /** * @@ -33,11 +33,9 @@ class ConstrainedFields(private val classHoldingConstraints: Class<*>) { * Add bean validation constraints for the field beanPropertyName to the descriptor */ fun addConstraints(fieldDescriptor: FieldDescriptor, beanPropertyName: String): FieldDescriptor = - fieldDescriptor.attributes( - key(CONSTRAINTS_KEY) - .value(this.validatorConstraintResolver.resolveForProperty(beanPropertyName, - classHoldingConstraints)) - ) + fieldDescriptor + .attributes(key(CONSTRAINTS_KEY).value(this.validatorConstraintResolver.resolveForProperty( + beanPropertyName, classHoldingConstraints))) private fun beanPropertyNameFromPath(path: String) = if (path.contains(SLASH_NOTATION_DELIMITER)) 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 7585b8a1..d534a581 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 @@ -76,7 +76,6 @@ internal class ConstrainedFieldsTest { private data class SomeWithConstraints( @field:NotEmpty val nonEmpty: String, - val nested: SomeWithConstraints? ) } From d1e5d58151b3657f5a3ae5793451755f149ef17f Mon Sep 17 00:00:00 2001 From: Lenard Gaida Date: Fri, 17 Jan 2020 14:56:33 +0100 Subject: [PATCH 4/6] fix indentation so kotlin lint wont fail --- ...JsonSchemaFromFieldDescriptorsGenerator.kt | 10 ++--- ...SchemaFromFieldDescriptorsGeneratorTest.kt | 8 ++-- ...RestDocumentationWrapperIntegrationTest.kt | 4 +- .../apispec/ResourceSnippetIntegrationTest.kt | 44 +++++++++---------- .../apispec/openapi3/OpenApi3Generator.kt | 2 +- .../apispec/openapi3/OpenApi3GeneratorTest.kt | 20 ++++----- .../restdocs/apispec/ConstrainedFieldsTest.kt | 8 ++-- 7 files changed, 47 insertions(+), 49 deletions(-) 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 2a4e3350..ac3f7ac8 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 @@ -18,7 +18,6 @@ import org.everit.json.schema.Schema import org.everit.json.schema.StringSchema import org.everit.json.schema.internal.JSONPrinter import java.io.StringWriter -import java.util.* import java.util.Collections.emptyList import java.util.function.Predicate @@ -38,8 +37,8 @@ class JsonSchemaFromFieldDescriptorsGenerator { val schemaTitle = if (title.isNullOrEmpty()) rootElementName else title val schema = traverse(emptyList(), - jsonFieldPaths, - ObjectSchema.builder().title(schemaTitle) as ObjectSchema.Builder) + jsonFieldPaths, + ObjectSchema.builder().title(schemaTitle) as ObjectSchema.Builder) return toFormattedString(unWrapRootArray(jsonFieldPaths, schema)) } @@ -52,8 +51,7 @@ class JsonSchemaFromFieldDescriptorsGenerator { return "" } - private fun replaceRootElementAndSlashes(fieldDescriptors: List, - rootElementName: String): List { + private fun replaceRootElementAndSlashes(fieldDescriptors: List, rootElementName: String): List { return fieldDescriptors .filter { fieldDescriptor -> fieldDescriptor.path != rootElementName } .map { fieldDescriptor -> @@ -64,7 +62,7 @@ class JsonSchemaFromFieldDescriptorsGenerator { fieldDescriptor.optional, fieldDescriptor.ignored, fieldDescriptor.attributes - ) + ) } } 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 2810618a..044fa950 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 @@ -256,7 +256,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { FieldDescriptor("id", "some", "STRING"), FieldDescriptor("id", "some", "NULL"), FieldDescriptor("id", "some", "BOOLEAN") - ) + ) } private fun givenFieldDescriptorWithXpathDescriptors() { @@ -265,7 +265,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { FieldDescriptor("root/a", "some a", "OBJECT"), FieldDescriptor("root/a/suba", "some sub a", "STRING"), FieldDescriptor("root/b", "some b", "STRING") - ) + ) } private fun givenFieldDescriptorsWithConstraints() { @@ -275,8 +275,8 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { Constraint( NotNull::class.java.name, emptyMap() - ) - ) + ) + ) ) val constraintAttributeWithLength = diff --git a/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt b/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt index 1c5b73e3..be9680c9 100644 --- a/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt +++ b/restdocs-api-spec-mockmvc/src/test/kotlin/com/epages/restdocs/apispec/MockMvcRestDocumentationWrapperIntegrationTest.kt @@ -154,7 +154,7 @@ class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired private val mock "count": 1 }""".trimIndent() ) - ).andExpect(status().isOk) + ).andExpect(status().isOk) } private fun givenEndpointInvokedWithXml(flagValue: String = "true") { @@ -173,7 +173,7 @@ class MockMvcRestDocumentationWrapperIntegrationTest(@Autowired private val mock """.trimIndent() ) .characterEncoding("UTF-8") - ).andExpect(status().isOk) + ).andExpect(status().isOk) } private fun thenSnippetFileExists() { 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 41177ce5..83bdd839 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 @@ -28,7 +28,7 @@ import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RestController -import java.util.* +import java.util.UUID import javax.validation.constraints.NotEmpty @ExtendWith(SpringExtension::class) @@ -52,11 +52,11 @@ open class ResourceSnippetIntegrationTest { @PostMapping(path = ["/some/{someId}/other/{otherId}"]) fun doSomething( - @PathVariable someId: String, - @PathVariable otherId: Int?, - @RequestHeader("X-Custom-Header") customHeader: String, - @RequestBody testDataHolder: TestDataHolder - ): ResponseEntity> { + @PathVariable someId: String, + @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() @@ -74,11 +74,11 @@ open class ResourceSnippetIntegrationTest { consumes = [MediaType.APPLICATION_XML_VALUE], produces = [MediaType.APPLICATION_XML_VALUE]) fun doSomethingWithXml( - @PathVariable someId: String, - @PathVariable otherId: Int?, - @RequestHeader("X-Custom-Header") customHeader: String, - @RequestBody testDataHolder: TestDataHolder - ): ResponseEntity> { + @PathVariable someId: String, + @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() @@ -125,21 +125,21 @@ fun buildFullResourceSnippet(): ResourceSnippet { .requestHeaders( headerWithName("X-Custom-Header").description("A custom header"), headerWithName(ACCEPT).description("Accept") - ) + ) .responseHeaders( headerWithName("X-Custom-Header").description("A custom header"), headerWithName(CONTENT_TYPE).description("ContentType") - ) + ) .pathParameters( parameterWithName("someId").description("some id"), parameterWithName("otherId").description("otherId id").type(SimpleType.INTEGER) - ) + ) .links( linkWithRel("self").description("some"), linkWithRel("multiple").description("multiple") - ) + ) .build() - ) + ) } fun fieldDescriptorsForXmlRequest(): FieldDescriptors { @@ -149,7 +149,7 @@ fun fieldDescriptorsForXmlRequest(): FieldDescriptors { fields.withPath("testDataHolder/comment").type(JsonFieldType.STRING).description("the comment").optional(), fields.withPath("testDataHolder/flag").type(JsonFieldType.BOOLEAN).description("the flag"), fields.withPath("testDataHolder/count").type(JsonFieldType.NUMBER).description("the count") - ) + ) } fun fieldDescriptorsForXmlResponse(): FieldDescriptors { @@ -170,7 +170,7 @@ fun fieldDescriptorsForXmlResponse(): FieldDescriptors { fields.withPath("Resource/links/links/title").type(JsonFieldType.STRING).description("title of link"), fields.withPath("Resource/links/links/type").type(JsonFieldType.STRING).description("type of link"), fields.withPath("Resource/links/links/deprecation").type(JsonFieldType.STRING).description("deprecation of link") - ) + ) } fun buildFullResourceSnippetWithXml(): ResourceSnippet { @@ -185,15 +185,15 @@ fun buildFullResourceSnippetWithXml(): ResourceSnippet { .requestHeaders( headerWithName("X-Custom-Header").description("A custom header"), headerWithName(ACCEPT).description("Accept") - ) + ) .responseHeaders( headerWithName("X-Custom-Header").description("A custom header"), headerWithName(CONTENT_TYPE).description("ContentType") - ) + ) .pathParameters( parameterWithName("someId").description("some id"), parameterWithName("otherId").description("otherId id").type(SimpleType.INTEGER) - ) + ) /* Can not be used, since spring framework expects links to be json, if we try this with xml it will fail .links( @@ -201,5 +201,5 @@ fun buildFullResourceSnippetWithXml(): ResourceSnippet { linkWithRel("multiple").description("multiple") )*/ .build() - ) + ) } 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 2a5fe46a..d9e71407 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 @@ -130,7 +130,7 @@ object OpenApi3Generator { schemasToKeys, it.schema, generateSchemaName(path) - ) + ) ) } } 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 d5622a7e..78ad9e51 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 @@ -492,8 +492,8 @@ class OpenApi3GeneratorTest { tags = setOf("tag1", "tag2"), request = getProductRequest(), response = getProductResponse() - ) - ) + ) + ) } private fun givenGetProductResourceModelWithXmlResponse() { @@ -507,8 +507,8 @@ class OpenApi3GeneratorTest { tags = setOf("tag1", "tag2"), request = getProductRequest(), response = getProductXmlResponse() - ) - ) + ) + ) } private fun givenGetProductResourceModelWithJWTSecurityRequirement() { @@ -561,13 +561,13 @@ class OpenApi3GeneratorTest { path = "_id", description = "ID of the product", type = "STRING" - ), + ), FieldDescriptor( path = "description", description = "Product description, localized.", type = "STRING" - ) - ), + ) + ), example = """{ "_id": "123", "description": "Good stuff!" @@ -592,13 +592,13 @@ class OpenApi3GeneratorTest { path = "TestDataHolder/_id", description = "ID of the product", type = "STRING" - ), + ), FieldDescriptor( path = "TestDataHolder/description", description = "Product description, localized.", type = "STRING" - ) - ), + ) + ), example = """<_id>123Good stuff!""".trimMargin() ) } 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 d534a581..0f306477 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 @@ -74,8 +74,8 @@ internal class ConstrainedFieldsTest { } private data class SomeWithConstraints( - @field:NotEmpty - val nonEmpty: String, - val nested: SomeWithConstraints? - ) + @field:NotEmpty + val nonEmpty: String, + val nested: SomeWithConstraints? + ) } From b99938989816bce8348cf589acbd89480c95cf05 Mon Sep 17 00:00:00 2001 From: Lenard Gaida Date: Mon, 20 Jan 2020 08:52:06 +0100 Subject: [PATCH 5/6] refactoring test, reuse existing method with parameter --- .../apispec/openapi3/OpenApi3GeneratorTest.kt | 50 ++----------------- 1 file changed, 5 insertions(+), 45 deletions(-) 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 78ad9e51..608a604e 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 @@ -32,7 +32,7 @@ class OpenApi3GeneratorTest { whenOpenApiObjectGenerated() - thenGetProductByIdOperationIsValid() + thenGetProductByIdOperationIsValid("application/json") thenOAuth2SecuritySchemesPresent() thenInfoFieldsPresent() thenTagFieldsPresent() @@ -47,7 +47,7 @@ class OpenApi3GeneratorTest { whenOpenApiObjectGenerated() - thenGetProductByIdOperationIsValidWithXml() + thenGetProductByIdOperationIsValid("application/xml") thenOAuth2SecuritySchemesPresent() thenInfoFieldsPresent() thenTagFieldsPresent() @@ -161,7 +161,7 @@ class OpenApi3GeneratorTest { thenOpenApiSpecIsValid() } - fun thenGetProductByIdOperationIsValid() { + fun thenGetProductByIdOperationIsValid(contentType: String) { val productGetByIdPath = "paths./products/{id}.get" then(openApiJsonPathContext.read>("$productGetByIdPath.tags")).isNotNull() then(openApiJsonPathContext.read("$productGetByIdPath.operationId")).isNotNull() @@ -185,48 +185,8 @@ class OpenApi3GeneratorTest { then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.description")).isNotNull() then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.headers.SIGNATURE.schema.type")).isNotNull() - then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/json.schema.\$ref")).isNotNull() - then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/json.examples.test.value")).isNotNull() - - then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_clientCredentials").flatMap { it }).containsOnly( - "prod:r") - then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_authorizationCode").flatMap { it }).containsOnly( - "prod:r") - } - - fun thenGetProductByIdOperationIsValidWithXml() { - val productGetByIdPath = "paths./products/{id}.get" - then(openApiJsonPathContext.read>("$productGetByIdPath.tags")).isNotNull() - then(openApiJsonPathContext.read("$productGetByIdPath.operationId")).isNotNull() - then(openApiJsonPathContext.read("$productGetByIdPath.summary")).isNotNull() - then(openApiJsonPathContext.read("$productGetByIdPath.description")).isNotNull() - then(openApiJsonPathContext.read("$productGetByIdPath.deprecated")).isNull() - - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'id')].in")).containsOnly( - "path") - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'id')].required")).containsOnly( - true) - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'locale')].in")).containsOnly( - "query") - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'locale')].required")).containsOnly( - false) - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'locale')].schema.type")).containsOnly( - "string") - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].in")).containsOnly( - "header") - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].required")).containsOnly( - true) - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].example")).containsOnly( - "some example") - then(openApiJsonPathContext.read>("$productGetByIdPath.parameters[?(@.name == 'Authorization')].schema.type")).containsOnly( - "string") - - then(openApiJsonPathContext.read("$productGetByIdPath.requestBody")).isNull() - - then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.description")).isNotNull() - then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.headers.SIGNATURE.schema.type")).isNotNull() - then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/xml.schema.\$ref")).isNotNull() - then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.application/xml.examples.test.value")).isNotNull() + then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.$contentType.schema.\$ref")).isNotNull() + then(openApiJsonPathContext.read("$productGetByIdPath.responses.200.content.$contentType.examples.test.value")).isNotNull() then(openApiJsonPathContext.read>>("$productGetByIdPath.security[*].oauth2_clientCredentials").flatMap { it }).containsOnly( "prod:r") From fc31b9275365fa910160901cd393e19e7ac53be3 Mon Sep 17 00:00:00 2001 From: Lenard Gaida Date: Mon, 20 Jan 2020 14:27:51 +0100 Subject: [PATCH 6/6] fix string initialization for xml root element and schema title --- .../jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ac3f7ac8..6c959ac2 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 @@ -26,7 +26,7 @@ class JsonSchemaFromFieldDescriptorsGenerator { fun generateSchema(fieldDescriptors: List, title: String? = null): String { var workingFieldDescriptors = fieldDescriptors - var rootElementName = "" + var rootElementName: String? = null if (fieldDescriptors.any { fieldDescriptor -> fieldDescriptor.path.contains('/') }) { rootElementName = findRootElementName(fieldDescriptors) workingFieldDescriptors = replaceRootElementAndSlashes(fieldDescriptors, rootElementName) @@ -35,7 +35,7 @@ class JsonSchemaFromFieldDescriptorsGenerator { val jsonFieldPaths = reduceFieldDescriptors(workingFieldDescriptors) .map { JsonFieldPath.compile(it) } - val schemaTitle = if (title.isNullOrEmpty()) rootElementName else title + val schemaTitle = if (!rootElementName.isNullOrEmpty()) rootElementName else title val schema = traverse(emptyList(), jsonFieldPaths, ObjectSchema.builder().title(schemaTitle) as ObjectSchema.Builder)