From d68049a4cf00743430308a467b5fec89959f5ccd Mon Sep 17 00:00:00 2001 From: Thomas Wimmer Date: Thu, 27 Feb 2020 21:40:51 +0100 Subject: [PATCH] Feature/87 support enums in generators (#130) * Add type property to enum schemas in generated JSON schema. * Add test to ensure that enum values are generated in OpenApi20Generator. * Add test to ensure that enum values are generated in OpenApi3Generator. * Fixed lint errors. * Fixed nondeterministic test failure. * Use CombinedSchema to set type property for Enum schemas. --- ...JsonSchemaFromFieldDescriptorsGenerator.kt | 6 +++- ...SchemaFromFieldDescriptorsGeneratorTest.kt | 10 +++++- .../openapi2/OpenApi20GeneratorTest.kt | 35 +++++++++++++++++-- .../apispec/openapi3/OpenApi3GeneratorTest.kt | 31 ++++++++++++++++ 4 files changed, 78 insertions(+), 4 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 032cd44b..a9c61a69 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 @@ -243,7 +243,11 @@ class JsonSchemaFromFieldDescriptorsGenerator { "string" -> StringSchema.builder() .minLength(minLengthString(this)) .maxLength(maxLengthString(this)) - "enum" -> EnumSchema.builder().possibleValues(this.attributes.enumValues) + "enum" -> CombinedSchema.oneOf( + listOf( + StringSchema.builder().build(), + EnumSchema.builder().possibleValues(this.attributes.enumValues).build()) + ).isSynthetic(true) else -> throw IllegalArgumentException("unknown field type $type") } 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 2299af32..dd84baa5 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 @@ -12,6 +12,7 @@ import org.everit.json.schema.ArraySchema import org.everit.json.schema.ObjectSchema import org.everit.json.schema.Schema import org.everit.json.schema.StringSchema +import org.everit.json.schema.CombinedSchema import org.everit.json.schema.EnumSchema import org.everit.json.schema.ValidationException import org.everit.json.schema.loader.SchemaLoader @@ -203,7 +204,14 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { then(schema).isInstanceOf(ObjectSchema::class.java) val objectSchema = schema as ObjectSchema? - then(objectSchema!!.propertySchemas["some"]).isInstanceOf(EnumSchema::class.java) + + val enumSchema = objectSchema!!.propertySchemas["some"] + then(enumSchema).isInstanceOf(CombinedSchema::class.java) + + val subschemas = (enumSchema as CombinedSchema).subschemas.toList() + then(subschemas).hasSize(2) + then(subschemas).extracting("class").containsOnlyOnce(EnumSchema::class.java) + then(subschemas).extracting("class").containsOnlyOnce(StringSchema::class.java) thenSchemaIsValid() thenSchemaValidatesJson("""{ some: "ENUM_VALUE_1" }""") 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 e611d70d..3173958f 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 @@ -14,6 +14,7 @@ import com.epages.restdocs.apispec.model.SecurityRequirements import com.epages.restdocs.apispec.model.Schema import com.epages.restdocs.apispec.model.SecurityType.BASIC import com.epages.restdocs.apispec.model.SecurityType.OAUTH2 +import com.epages.restdocs.apispec.model.Attributes import com.fasterxml.jackson.module.kotlin.readValue import io.swagger.models.Model import io.swagger.models.Path @@ -24,6 +25,7 @@ import io.swagger.models.auth.OAuth2Definition import io.swagger.models.parameters.BodyParameter import io.swagger.models.parameters.Parameter import io.swagger.models.parameters.PathParameter +import io.swagger.models.properties.StringProperty import io.swagger.parser.Swagger20Parser import io.swagger.util.Json import org.assertj.core.api.Assertions.tuple @@ -209,6 +211,16 @@ class OpenApi20GeneratorTest { thenValidateOpenApi(openapi) } + @Test + fun `should include enum values in schemas`() { + val api = givenPostProductResourceModelWithCustomSchemaNames() + + val openapi = whenOpenApiObjectGenerated(api) + + thenEnumValuesAreSetInRequestAndResponse(openapi) + thenValidateOpenApi(openapi) + } + private fun whenExtractOrFindSchema(schemaNameAndSchemaMap: MutableMap, ordersSchema: Model, shopsSchema: Model) { OpenApi20Generator.extractOrFindSchema(schemaNameAndSchemaMap, ordersSchema, OpenApi20Generator.generateSchemaName("/orders")) OpenApi20Generator.extractOrFindSchema(schemaNameAndSchemaMap, shopsSchema, OpenApi20Generator.generateSchemaName("/shops")) @@ -327,11 +339,11 @@ class OpenApi20GeneratorTest { val requestSchemaRef = requestBody.schema.reference then(requestSchemaRef).startsWith("${SCHEMA_JSONPATH_PREFIX}products") val requestSchemaRefName = requestSchemaRef.replace(SCHEMA_JSONPATH_PREFIX, "") - then(definitions.get(requestSchemaRefName)!!.properties.keys).containsExactlyInAnyOrder("description", "price") + then(definitions.get(requestSchemaRefName)!!.properties.keys).containsExactlyInAnyOrder("description", "price", "someEnum") then(successfulPostResponse.responseSchema.reference).startsWith("${SCHEMA_JSONPATH_PREFIX}products") val responseSchemaRefName = successfulPostResponse.responseSchema.reference.replace(SCHEMA_JSONPATH_PREFIX, "") - then(definitions.get(responseSchemaRefName)!!.properties.keys).containsExactlyInAnyOrder("_id", "description", "price") + then(definitions.get(responseSchemaRefName)!!.properties.keys).containsExactlyInAnyOrder("_id", "description", "price", "someEnum") } private fun thenGetProductWith400ResponseIsGenerated(openapi: Swagger, api: List) { @@ -392,6 +404,13 @@ class OpenApi20GeneratorTest { then(openapi.definitions.keys).contains("ProductResponse2") } + private fun thenEnumValuesAreSetInRequestAndResponse(openapi: Swagger) { + then(openapi.definitions["ProductRequest"]?.properties?.keys ?: emptyList()).contains("someEnum") + then((openapi.definitions["ProductRequest"]!!.properties["someEnum"] as StringProperty).enum).containsExactly("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE") + then(openapi.definitions["ProductResponse"]?.properties?.keys ?: emptyList()).contains("someEnum") + then((openapi.definitions["ProductResponse"]!!.properties["someEnum"] as StringProperty).enum).containsExactly("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE") + } + private fun givenGetProductResourceModel(): List { return listOf( ResourceModel( @@ -613,6 +632,12 @@ class OpenApi20GeneratorTest { path = "price.amount", description = "Product price.", type = "NUMBER" + ), + FieldDescriptor( + path = "someEnum", + description = "Some enum description", + type = "enum", + attributes = Attributes(enumValues = listOf("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE")) ) ), example = example @@ -798,6 +823,12 @@ class OpenApi20GeneratorTest { path = "price.amount", description = "Product price.", type = "NUMBER" + ), + FieldDescriptor( + path = "someEnum", + description = "Some enum description", + type = "enum", + attributes = Attributes(enumValues = listOf("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE")) ) ), example = getProductPayloadExample() 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 d7433de2..1f696ff3 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 @@ -11,6 +11,7 @@ import com.epages.restdocs.apispec.model.ResponseModel import com.epages.restdocs.apispec.model.SecurityRequirements import com.epages.restdocs.apispec.model.SecurityType import com.epages.restdocs.apispec.model.Schema +import com.epages.restdocs.apispec.model.Attributes import com.jayway.jsonpath.Configuration import com.jayway.jsonpath.DocumentContext import com.jayway.jsonpath.JsonPath @@ -166,6 +167,16 @@ class OpenApi3GeneratorTest { thenOpenApiSpecIsValid() } + @Test + fun `should include enum values in schemas`() { + givenPatchProductResourceModelWithCustomSchemaNames() + + whenOpenApiObjectGenerated() + + thenEnumValuesAreSetInRequestAndResponse() + thenOpenApiSpecIsValid() + } + fun thenGetProductByIdOperationIsValid() { val productGetByIdPath = "paths./products/{id}.get" then(openApiJsonPathContext.read>("$productGetByIdPath.tags")).isNotNull() @@ -244,6 +255,14 @@ class OpenApi3GeneratorTest { then(schemas.keys).contains("ProductResponse2") } + private fun thenEnumValuesAreSetInRequestAndResponse() { + val requestEnum = openApiJsonPathContext.read>("components.schemas.ProductRequest.properties.someEnum") + then(requestEnum["enum"] as List<*>).containsExactly("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE") + + val responseEnum = openApiJsonPathContext.read>("components.schemas.ProductResponse.properties.someEnum") + then(responseEnum["enum"] as List<*>).containsExactly("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE") + } + private fun whenOpenApiObjectGenerated() { openApiSpecJsonString = OpenApi3Generator.generateAndSerialize( resources = resources, @@ -560,6 +579,12 @@ class OpenApi3GeneratorTest { path = "description", description = "Product description, localized.", type = "STRING" + ), + FieldDescriptor( + path = "someEnum", + description = "Some enum description", + type = "enum", + attributes = Attributes(enumValues = listOf("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE")) ) ), example = """{ @@ -610,6 +635,12 @@ class OpenApi3GeneratorTest { path = "description1", description = "Product description, localized.", type = "STRING" + ), + FieldDescriptor( + path = "someEnum", + description = "Some enum description", + type = "enum", + attributes = Attributes(enumValues = listOf("FIRST_VALUE", "SECOND_VALUE", "THIRD_VALUE")) ) ), contentType = "application/json",