diff --git a/README.md b/README.md index 878b771a..02565556 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ openapi { //6 basePath = '/api' title = 'My API' description = 'My API description' + tagDescriptionsPropertiesFile = 'src/docs/tag-descriptions.yaml' version = '1.0.0' format = 'json' } @@ -116,6 +117,7 @@ openapi3 { server = 'https://localhost:8080' title = 'My API' description = 'My API description' + tagDescriptionsPropertiesFile = 'src/docs/tag-descriptions.yaml' version = '0.1.0' format = 'yaml' } @@ -334,6 +336,7 @@ title | The title of the application. Used for the `title` attribute in the [Inf description | A description of the application. Used for the `description` attribute in the [Info object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#info-object) | empty version | The version of the api. Used for the `version` attribute in the [Info object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#info-object) | project version format | The format of the output OpenAPI file - supported values are `json` and `yaml` | `json` +tagDescriptionsPropertiesFile | A yaml file mapping tag names to descriptions. These are populated into the top level ` [Tags attribute](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#tag-object) | no default - if not provided no tags created. separatePublicApi | Should the plugin generate an additional OpenAPI specification file that does not contain the resources marked as private | `false` outputDirectory | The output directory | `build/openapi` snippetsDirectory | The directory Spring REST Docs generated the snippets to | `build/generated-snippets` diff --git a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApi3Task.kt b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApi3Task.kt index 83cd5ba6..4b290029 100644 --- a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApi3Task.kt +++ b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApi3Task.kt @@ -23,6 +23,7 @@ open class OpenApi3Task : OpenApiBaseTask() { servers = servers, title = title, description = apiDescription, + tagDescriptions = tagDescriptions, version = apiVersion, oauth2SecuritySchemeDefinition = oauth2SecuritySchemeDefinition, format = format diff --git a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiBaseTask.kt b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiBaseTask.kt index d8607421..55a48373 100644 --- a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiBaseTask.kt +++ b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiBaseTask.kt @@ -20,6 +20,10 @@ abstract class OpenApiBaseTask : ApiSpecTask() { @Optional lateinit var format: String + @Input + @Optional + lateinit var tagDescriptions: Map + @Input @Optional var oauth2SecuritySchemeDefinition: PluginOauth2Configuration? = null @@ -31,6 +35,7 @@ abstract class OpenApiBaseTask : ApiSpecTask() { oauth2SecuritySchemeDefinition = extension.oauth2SecuritySchemeDefinition title = extension.title apiDescription = extension.description + tagDescriptions = extension.tagDescriptions() apiVersion = extension.version } } diff --git a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiExtension.kt b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiExtension.kt index ec3e741b..27f235bf 100644 --- a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiExtension.kt +++ b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiExtension.kt @@ -17,6 +17,7 @@ abstract class OpenApiBaseExtension(project: Project) : ApiSpecExtension(project var title = "API documentation" var version = project.version as? String ?: "1.0.0" var description: String? = null + var tagDescriptionsPropertiesFile: String? = null var format = "json" @@ -31,6 +32,10 @@ abstract class OpenApiBaseExtension(project: Project) : ApiSpecExtension(project } } + fun tagDescriptions(): Map { + return tagDescriptionsPropertiesFile?.let { objectMapper.readValue(project.file(it)) } ?: emptyMap() + } + private fun scopeDescriptionSource(scopeDescriptionsPropertiesFile: File): Map { return scopeDescriptionsPropertiesFile.let { objectMapper.readValue>(it) } ?: emptyMap() } diff --git a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiTask.kt b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiTask.kt index c97357d2..cc51982f 100644 --- a/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiTask.kt +++ b/restdocs-api-spec-gradle-plugin/src/main/kotlin/com/epages/restdocs/apispec/gradle/OpenApiTask.kt @@ -32,6 +32,7 @@ open class OpenApiTask : OpenApiBaseTask() { schemes = schemes.toList(), title = title, description = apiDescription, + tagDescriptions = tagDescriptions, version = apiVersion, oauth2SecuritySchemeDefinition = oauth2SecuritySchemeDefinition, format = format 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 4dceb9b2..26ec96bf 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 @@ -109,6 +109,7 @@ class RestdocsOpenApi3TaskTest : RestdocsOpenApiTaskTestBase() { servers = [ { url = "http://some.api" } ] title = '$title' description = '$description' + tagDescriptionsPropertiesFile = "tagDescriptions.yaml" version = '$version' format = '$format' separatePublicApi = $separatePublicApi diff --git a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTest.kt b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTest.kt index 09c04c78..5b292b7b 100644 --- a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTest.kt +++ b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTest.kt @@ -18,6 +18,7 @@ class RestdocsOpenApiTaskTest : RestdocsOpenApiTaskTestBase() { schemes = ${schemes.joinToString(",", "['", "']")} title = '$title' description = '$description' + tagDescriptionsPropertiesFile = "tagDescriptions.yaml" version = '$version' format = '$format' separatePublicApi = $separatePublicApi diff --git a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTestBase.kt b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTestBase.kt index 5f27c277..9be2fc4a 100644 --- a/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTestBase.kt +++ b/restdocs-api-spec-gradle-plugin/src/test/kotlin/com/epages/restdocs/apispec/gradle/RestdocsOpenApiTaskTestBase.kt @@ -55,6 +55,7 @@ abstract class RestdocsOpenApiTaskTestBase { @Test open fun `should run openapi task`() { givenBuildFileWithOpenApiClosure() + givenTagsTextFile() givenResourceSnippet() whenPluginExecuted() @@ -80,6 +81,7 @@ abstract class RestdocsOpenApiTaskTestBase { fun `should run openapi task with yaml format`() { format = "yaml" givenBuildFileWithOpenApiClosure() + givenTagsTextFile() givenResourceSnippet() whenPluginExecuted() @@ -92,6 +94,7 @@ abstract class RestdocsOpenApiTaskTestBase { fun `should generate separate public api specification`() { separatePublicApi = true givenBuildFileWithOpenApiClosure() + givenTagsTextFile() givenResourceSnippet() givenPrivateResourceSnippet() @@ -105,6 +108,7 @@ abstract class RestdocsOpenApiTaskTestBase { @Test fun `should consider security definitions`() { givenBuildFileWithOpenApiClosureAndSecurityDefinitions() + givenTagsTextFile() givenResourceSnippet() givenScopeTextFile() @@ -124,7 +128,14 @@ abstract class RestdocsOpenApiTaskTestBase { """.trimIndent() ) } - + private fun givenTagsTextFile() { + testProjectDir.resolve("tagDescriptions.yaml").toFile().writeText( + """ + "tag1": "tag1 description" + "tag2": "tag2 description" + """.trimIndent() + ) + } protected fun thenOpenApiTaskSuccessful() { then(result.task(":$taskName")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) } 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 b8d32bbc..4d97b931 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 @@ -20,6 +20,7 @@ import io.swagger.models.RefModel import io.swagger.models.Response import io.swagger.models.Scheme import io.swagger.models.Swagger +import io.swagger.models.Tag import io.swagger.models.auth.ApiKeyAuthDefinition import io.swagger.models.auth.BasicAuthDefinition import io.swagger.models.auth.OAuth2Definition @@ -44,6 +45,7 @@ object OpenApi20Generator { schemes: List = listOf("http"), title: String = "API", description: String? = null, + tagDescriptions: Map = emptyMap(), version: String = "1.0.0", oauth2SecuritySchemeDefinition: Oauth2Configuration? = null ): Swagger { @@ -57,6 +59,10 @@ object OpenApi20Generator { this.description = description this.version = version } + this.tags(tagDescriptions.map { Tag().apply { + this.name = it.key + this.description = it.value + } }) paths = generatePaths( resources, oauth2SecuritySchemeDefinition @@ -78,11 +84,12 @@ object OpenApi20Generator { schemes: List = listOf("http"), title: String = "API", description: String? = null, + tagDescriptions: Map = emptyMap(), version: String = "1.0.0", oauth2SecuritySchemeDefinition: Oauth2Configuration? = null, format: String ): String { - val specification = generate(resources, basePath, host, schemes, title, description, version, oauth2SecuritySchemeDefinition) + val specification = generate(resources, basePath, host, schemes, title, description, tagDescriptions, version, oauth2SecuritySchemeDefinition) return ApiSpecificationWriter.serialize(format, specification) } 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 3949810e..ceb52167 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 @@ -23,6 +23,7 @@ import io.swagger.models.parameters.Parameter import io.swagger.models.parameters.PathParameter import io.swagger.parser.Swagger20Parser import io.swagger.util.Json +import org.assertj.core.api.Assertions.tuple import org.assertj.core.api.BDDAssertions.then import org.junit.jupiter.api.Test @@ -30,6 +31,21 @@ private const val SCHEMA_JSONPATH_PREFIX = "#/definitions/" class OpenApi20GeneratorTest { + @Test + fun `should have parent tags generated for openapi`() { + val api = givenGetProductResourceModel() + + val openapi = whenOpenApiObjectGenerated(api) + + with(openapi) { + then(this.tags).extracting("name", "description") + .containsExactly( + tuple("tag1", "tag1 description"), + tuple("tag2", "tag2 description") + ) + } + } + @Test fun `should convert single resource model to openapi`() { val api = givenGetProductResourceModel() @@ -143,7 +159,8 @@ class OpenApi20GeneratorTest { "http://example.com/authorize", arrayOf("application", "accessCode") ), - description = "API description" + description = "API description", + tagDescriptions = mapOf("tag1" to "tag1 description", "tag2" to "tag2 description") ) println(ApiSpecificationWriter.serialize("yaml", openapi)) 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 c58a07a0..2beceb35 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 @@ -36,6 +36,7 @@ import io.swagger.v3.oas.models.parameters.RequestBody import io.swagger.v3.oas.models.responses.ApiResponse import io.swagger.v3.oas.models.responses.ApiResponses import io.swagger.v3.oas.models.servers.Server +import io.swagger.v3.oas.models.tags.Tag import java.util.Comparator.comparing import java.util.Comparator.comparingInt @@ -46,6 +47,7 @@ object OpenApi3Generator { servers: List, title: String = "API", description: String? = null, + tagDescriptions: Map = emptyMap(), version: String = "1.0.0", oauth2SecuritySchemeDefinition: Oauth2Configuration? = null ): OpenAPI { @@ -57,6 +59,10 @@ object OpenApi3Generator { this.description = description this.version = version } + this.tags(tagDescriptions.map { Tag().apply { + this.name = it.key + this.description = it.value + } }) paths = generatePaths( resources, oauth2SecuritySchemeDefinition @@ -71,6 +77,7 @@ object OpenApi3Generator { servers: List, title: String = "API", description: String? = null, + tagDescriptions: Map = emptyMap(), version: String = "1.0.0", oauth2SecuritySchemeDefinition: Oauth2Configuration? = null, format: String @@ -81,6 +88,7 @@ object OpenApi3Generator { servers = servers, title = title, description = description, + tagDescriptions = tagDescriptions, version = version, oauth2SecuritySchemeDefinition = oauth2SecuritySchemeDefinition )) 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 8ea2b5c8..0147333c 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 @@ -35,6 +35,7 @@ class OpenApi3GeneratorTest { thenGetProductByIdOperationIsValid() thenSecuritySchemesPresent() thenInfoFieldsPresent() + thenTagFieldsPresent() thenServersPresent() thenOpenApiSpecIsValid() } @@ -156,6 +157,13 @@ class OpenApi3GeneratorTest { then(openApiJsonPathContext.read("info.version")).isEqualTo("1.0.0") } + private fun thenTagFieldsPresent() { + then(openApiJsonPathContext.read("tags[0].name")).isEqualTo("tag1") + then(openApiJsonPathContext.read("tags[0].description")).isEqualTo("tag1 description") + then(openApiJsonPathContext.read("tags[1].name")).isEqualTo("tag2") + then(openApiJsonPathContext.read("tags[1].description")).isEqualTo("tag2 description") + } + private fun thenSecuritySchemesPresent() { then(openApiJsonPathContext.read("components.securitySchemes.oauth2.type")).isEqualTo("oauth2") then(openApiJsonPathContext.read>("components.securitySchemes.oauth2.flows")) @@ -176,7 +184,8 @@ class OpenApi3GeneratorTest { arrayOf("clientCredentials", "authorizationCode") ), format = "json", - description = "API Description" + description = "API Description", + tagDescriptions = mapOf("tag1" to "tag1 description", "tag2" to "tag2 description") ) println(openApiSpecJsonString)