Skip to content

Commit

Permalink
minor: (#173) remove incorrect Json2YamlTask and make the output form…
Browse files Browse the repository at this point in the history
…at configurable (#175)

* minor: (#173) remove incorrect Json2YamlTask and make the output format configurable
* update docs
* add workaround for lazy task configuration of the `generateOpenApiDocs` task

---------

Signed-off-by: Clemens Grabmann <[email protected]>
  • Loading branch information
cgrabmann authored Aug 22, 2023
1 parent 3d632f9 commit b3845bc
Show file tree
Hide file tree
Showing 14 changed files with 249 additions and 121 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,37 @@ plugins {
}
```

The plugin has to be applied to a module that provides a Spring Boot application which the plugin will try to start using a custom Spring Boot run configuration.
The plugin has to be applied to a module that provides a Spring Boot application which the plugin will try to start using a custom Spring Boot run configuration.
This custom run configuration will be started in a dummy working directory to work around a issue currently present in the springdoc plugin. You can find more information [here](https://github.com/cloudflightio/autoconfigure-gradle-plugin/issues/171).
The dummy working directory is created with a task called `createDummyForkedSpringBootWorkingDir`. Various other tasks are automatically configured to depend on this, since they access the dummy directory for some reason.

<details>
<summary>If you run into some problems with the task, try adding it as a dependency to your task by adding the following.</summary>

```groovy
tasks.named("your-task-name") {
dependsOn("createDummyForkedSpringBootWorkingDir")
}
```

If you have multiple tasks that need to depend on it you can do:

```groovy
def taskList = ["your-task-1", "your-task-2"]
tasks.matching { taskList.contains(it.name) }.all {
dependsOn("createDummyForkedSpringBootWorkingDir")
}
```
</details>

The springdoc plugin is automatically configured to generate the open-api spec in `YAML` format. If you prefer the `JSON` format you can easily change that by using our extension:
```groovy
import io.cloudflight.gradle.autoconfigure.springdoc.openapi.OpenApiFormat
openApiConfigure {
fileFormat = OpenApiFormat.JSON
}
```

For generating the OpenAPI document the task `clfGenerateOpenApiDocumentation` has to be run.

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
package io.cloudflight.gradle.autoconfigure.extentions.gradle.api.tasks

import org.gradle.api.DomainObjectCollection
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.NamedDomainObjectSet
import org.gradle.api.Task
import org.gradle.api.tasks.TaskCollection
import org.gradle.api.tasks.TaskProvider
import kotlin.reflect.KClass


/**
* @see TaskCollection.withType
*/
internal fun <T : Task, S : T> TaskCollection<T>.withType(klass: KClass<S>): NamedDomainObjectSet<S> = this.withType(klass.java)


/**
* @see TaskCollection.withType
*/
internal fun <T : Task, S : T> TaskCollection<T>.withType(klass: KClass<S>, configuration: (it: S) -> Unit): DomainObjectCollection<S> = this.withType(klass.java, configuration)

/**
* @see TaskCollection.named
*/
internal fun <T : Task, S : T> TaskCollection<T>.named(name: String, klass: KClass<S>): TaskProvider<S> = this.named(name, klass.java)


/**
* @see TaskCollection.named
*/
internal fun <T : Task, S : T> TaskCollection<T>.named(name: String, klass: KClass<S>): TaskProvider<S> = this.named(name, klass.java)
internal fun <T: Task, S : T> TaskCollection<T>.named(name: String, klass: KClass<S>, configuration: (it: S) -> Unit): NamedDomainObjectProvider<S> = this.named(name, klass.java, configuration)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.cloudflight.gradle.autoconfigure.springdoc.openapi

import org.gradle.api.provider.Property

enum class OpenApiFormat(val extension: String) {
YAML("yaml"),
JSON("json")
}

abstract class SpringDocOpenApiConfigureExtension {
abstract val fileFormat: Property<OpenApiFormat>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package io.cloudflight.gradle.autoconfigure.springdoc.openapi

import com.github.psxpaul.task.JavaExecFork
import io.cloudflight.gradle.autoconfigure.AutoConfigureGradlePlugin.Companion.TASK_GROUP
import io.cloudflight.gradle.autoconfigure.extentions.gradle.api.named
import io.cloudflight.gradle.autoconfigure.extentions.gradle.api.withType
import io.cloudflight.gradle.autoconfigure.extentions.gradle.api.tasks.named
import io.cloudflight.gradle.autoconfigure.extentions.gradle.api.tasks.withType
import io.cloudflight.gradle.autoconfigure.java.JavaConfigurePlugin
import io.cloudflight.gradle.autoconfigure.util.addApiDocumentationPublication
import org.gradle.api.Plugin
Expand All @@ -14,6 +14,7 @@ import org.gradle.api.tasks.TaskProvider
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springdoc.openapi.gradle.plugin.OpenApiExtension
import org.springdoc.openapi.gradle.plugin.OpenApiGeneratorTask
import org.springdoc.openapi.gradle.plugin.OpenApiGradlePlugin
import org.springframework.boot.gradle.plugin.SpringBootPlugin
import java.net.ServerSocket
Expand All @@ -25,21 +26,16 @@ class SpringDocOpenApiConfigurePlugin : Plugin<Project> {
target.plugins.apply(SpringBootPlugin::class.java)
target.plugins.apply(OpenApiGradlePlugin::class.java)

val extension = target.extensions.create(EXTENSION_NAME, SpringDocOpenApiConfigureExtension::class.java)
extension.fileFormat.convention(OpenApiFormat.YAML);
val openapi = target.extensions.getByType(OpenApiExtension::class.java)
configureOpenApiExtension(openapi, target, target.name)
val openApiTask = target.tasks.named("generateOpenApiDocs")
val json2Yaml: TaskProvider<out Task> =
target.tasks.register("clfJsonToYaml", Json2YamlTask::class.java) { task ->
with(openapi) {
task.inputFile.set(outputDir.file(outputFileName))
task.outputFile.set(outputDir.file(outputFileName.map { it.replace(".json", ".yaml") }))
task.dependsOn(openApiTask)
}
}
configureOpenApiExtension(openapi, extension, target, target.name)
val openApiTask = target.tasks.named("generateOpenApiDocs", OpenApiGeneratorTask::class)
makeOpenApiTaskReactive(openApiTask, openapi)

val documentationTask = target.tasks.register("clfGenerateOpenApiDocumentation") {
it.group = TASK_GROUP
it.dependsOn(json2Yaml)
it.dependsOn(openApiTask)
}

target.tasks.withType(GenerateMavenPom::class) {
Expand All @@ -49,9 +45,25 @@ class SpringDocOpenApiConfigurePlugin : Plugin<Project> {
`setupWorkaroundFor#171`(target, openapi)

target.afterEvaluate {
configureJsonDocumentPublishing(openapi, target, openApiTask)
configureYamlDocumentPublishing(target, openapi, json2Yaml)
configureDocumentPublishing(openapi, target, openApiTask)
}
}

private fun makeOpenApiTaskReactive(openApiTask: TaskProvider<OpenApiGeneratorTask>, openapi: OpenApiExtension) {
// for some reason the springdoc plugin reads the values from the extension during task initialization and uses that as convention for the task properties,
// which results in some values being incorrect. Because of that we reconfigure the properties conventions to directly use the extension properties. And
// add conventions to the extension properties to the expected default values see: https://github.com/springdoc/springdoc-openapi-gradle-plugin/blob/master/src/main/kotlin/org/springdoc/openapi/gradle/plugin/OpenApiGeneratorTask.kt#L46
openApiTask.configure {
it.apiDocsUrl.convention(openapi.apiDocsUrl)
it.outputFileName.convention(openapi.outputFileName)
it.groupedApiMappings.convention(openapi.groupedApiMappings)
it.outputDir.convention(openapi.outputDir)
}

openapi.apiDocsUrl.convention("http://localhost:8080/v3/api-docs")
openapi.outputFileName.convention("openapi.json")
openapi.groupedApiMappings.convention(emptyMap())
openapi.outputDir.convention(openApiTask.flatMap { it.project.layout.buildDirectory })
}

private fun `setupWorkaroundFor#171`(target: Project, openapi: OpenApiExtension) {
Expand All @@ -72,7 +84,7 @@ class SpringDocOpenApiConfigurePlugin : Plugin<Project> {
// these tasks also need to depend on the createDirTask since they somehow access the dummy folder as well
val dependingTaskNames = setOf("resolveMainClassName", "processResources", "compileKotlin", "compileJava")

target.tasks.matching { dependingTaskNames.contains(it.name) }.configureEach {
target.tasks.matching { dependingTaskNames.contains(it.name) }.all {
it.dependsOn(createDirTask)
}

Expand All @@ -83,26 +95,34 @@ class SpringDocOpenApiConfigurePlugin : Plugin<Project> {

private fun configureOpenApiExtension(
openapi: OpenApiExtension,
configureExtension: SpringDocOpenApiConfigureExtension,
target: Project,
basename: String
) {
with(openapi) {
val serverPort = freeServerSocketPort()
val managementPort = freeServerSocketPort()

outputDir.set(target.layout.buildDirectory.dir("generated/resources/openapi"))
outputFileName.set("${basename}.json")
apiDocsUrl.set("http://localhost:${serverPort}/v3/api-docs")
customBootRun {
it.workingDir.set(target.layout.buildDirectory.dir("dummyForkedSpringBootWorkingDir"))
val serverPort = freeServerSocketPort()
val managementPort = freeServerSocketPort()
val outputFileName = configureExtension.fileFormat.map { "${basename}.${it.extension}" }
val docsUrl = openapi.outputFileName.map {
val basePath = "http://localhost:${serverPort}/v3/api-docs"
when {
it.endsWith(".${OpenApiFormat.JSON.extension}") -> basePath
it.endsWith(".${OpenApiFormat.YAML.extension}") -> "${basePath}.${OpenApiFormat.YAML.extension}"
else -> throw UnsupportedFormatException("The provided openapi filename '${it}' ends in an unsupported extension. Make sure you use 'yaml' or 'json'")
}
}

mapOf(
"--server.port" to serverPort,
"--management.server.port" to managementPort
).forEach { arg ->
customBootRun.args.add("${arg.key}=${arg.value}")
}
openapi.outputDir.set(target.layout.buildDirectory.dir("generated/resources/openapi"))
openapi.outputFileName.set(outputFileName)
openapi.apiDocsUrl.set(docsUrl)
openapi.customBootRun {
it.workingDir.set(target.layout.buildDirectory.dir("dummyForkedSpringBootWorkingDir"))
}

mapOf(
"--server.port" to serverPort,
"--management.server.port" to managementPort
).forEach { arg ->
openapi.customBootRun.args.add("${arg.key}=${arg.value}")
}
}

Expand All @@ -113,37 +133,36 @@ class SpringDocOpenApiConfigurePlugin : Plugin<Project> {
}
}

private fun configureJsonDocumentPublishing(
private fun configureDocumentPublishing(
openapi: OpenApiExtension,
target: Project,
task: TaskProvider<Task>,
task: TaskProvider<out Task>,
) {
addApiDocumentationPublication(
target,
task,
target.artifacts,
openapi.outputDir.get().toString(),
openapi.outputFileName.get().replace(".json", ""),
"json"
)
}
val format = openapi.outputFileName.map {
if (it.endsWith(".${OpenApiFormat.YAML.extension}")) {
OpenApiFormat.YAML
} else if (it.endsWith(".${OpenApiFormat.JSON.extension}")) {
OpenApiFormat.JSON
} else {
throw UnsupportedFormatException("The provided openapi filename '${it}' ends in an unsupported extension. Make sure you use 'yaml' or 'json'")
}
}

val basename = openapi.outputFileName.zip(format) { fileName, format ->
fileName.replace(".${format.extension}", "")
}

private fun configureYamlDocumentPublishing(
target: Project,
openapi: OpenApiExtension,
task: TaskProvider<out Task>
) {
addApiDocumentationPublication(
target,
task,
target.artifacts,
openapi.outputDir.get().toString(),
openapi.outputFileName.get().replace(".json", ""),
"yaml"
openapi.outputDir,
basename,
format
)
}

companion object {
const val EXTENSION_NAME = "openApiConfigure"
val logger: Logger = LoggerFactory.getLogger(SpringDocOpenApiConfigurePlugin::class.java)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.cloudflight.gradle.autoconfigure.springdoc.openapi

class UnsupportedFormatException(message: String) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package io.cloudflight.gradle.autoconfigure.util

import io.cloudflight.gradle.autoconfigure.springdoc.openapi.OpenApiFormat
import io.cloudflight.gradle.autoconfigure.swagger.SWAGGER_CLASSIFIER
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.PublishArtifact
import org.gradle.api.artifacts.dsl.ArtifactHandler
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.TaskProvider

internal fun addApiDocumentationPublication(
Expand All @@ -27,20 +29,23 @@ internal fun addApiDocumentationPublication(
}

internal fun addApiDocumentationPublication(
project: Project,
task: TaskProvider<out Task>,
artifacts: ArtifactHandler,
targetDir: String,
basename: String,
format: String
targetDir: DirectoryProperty,
basename: Provider<String>,
format: Provider<OpenApiFormat>
): PublishArtifact {
val fileName = basename.zip(format) { name, format ->
"${name}.${format.extension}"
}

return artifacts.add(
JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME,
project.file("$targetDir/${basename}.${format}")
targetDir.file(fileName)
) {
it.name = basename
it.name = basename.get()
it.classifier = SWAGGER_CLASSIFIER
it.type = format
it.type = format.get().extension
it.builtBy(task.get())
}
}
}
Loading

0 comments on commit b3845bc

Please sign in to comment.