Mike Wacker
12/16/2024, 9:53 PMsrc/main/resources/api.yml
and then build the generated interfaces. I've got it working, but I just want to check that I'm not doing anything here that violates Gradle best practices. Here is `build.gradle.kts`:
plugins {
`java-library`
id("org.openapi.generator") version "7.10.0"
}
// Create a task to validate the OpenAPI YAML.
openApiValidate {
inputSpec = "$projectDir/src/main/resources/api.yaml"
}
// Check that the Open API YAML is valid.
tasks.named("check") {
dependsOn("openApiValidate")
}
// Create a task to generate the JAX-RS API from the OpenAPI YAML.
openApiGenerate {
generatorName = "jaxrs-spec"
inputSpec = "$projectDir/src/main/resources/api.yaml"
outputDir = layout.buildDirectory.dir("generated/sources/openApi").get().asFile.absolutePath
// Generate only the apis and models. Don't generate the invokers or other supporting files.
apiPackage = "org.example.api"
modelPackage = "org.example.api"
globalProperties = mapOf(
"apis" to "",
"models" to "",
)
// Generate an async interface for the apis. Use builders for the models.
configOptions = mapOf(
"generateBuilders" to "true",
"hideGenerationTimestamp" to "true",
"interfaceOnly" to "true",
"sourceFolder" to "java/main",
"supportAsync" to "true",
"useJakartaEe" to "true",
"useSwaggerAnnotations" to "false",
"useTags" to "true",
)
}
// Compile the JAX-RS API that is generated from the OpenAPI YAML.
tasks.named("compileJava") {
dependsOn("openApiGenerate")
}
sourceSets {
main {
java {
srcDir(layout.buildDirectory.dir("generated/sources/openApi/java/main"))
}
}
}
repositories {
mavenCentral()
}
dependencies {
api(libs.jackson.annotations)
api(libs.jakartaAnnotation.api)
api(libs.jakartaValidation.api)
api(libs.jaxRs.api)
}
Only two small things I've noticed:
• I know $buildDir
is deprecated, but is there anything a bit more...elegant than layout.buildDirectory.dir("generated/sources/openApi").get().asFile.absolutePath
? Compare that to "$projectDir/src/main/resources/api.yaml"
.
• In IntelliJ, it says that build/generated/sources/openApi/java/main
is a "sources root", whereas build/generated/sources/annotationProcessor/java/main
(for projects that use an annotation processor) is a "generated sources root". Is there anything I should do differently when adding a srcDir
for generated sources? (But this is minor in the grand scheme of things.)Vampire
12/16/2024, 11:40 PMdependsOn
except with a lifecycle task on the left-hand side
Actually both problems are related. You add the explicit dependency because you did not properly wire task outputs to task inputs. But you only do it partly - always. What about other tasks that need source files besides compileJava
, for example sourcesJar
or any static code analyzer task that needs all sources, and so on. Any explicit dependsOn
without lifecycle task on the left-hand side is a code smell and a sign for doing something not properly, most often not wiring task ouputs to task inputs properly like here.
If the openaApiGenerate
task properly declares its output files which are the sources files, then directly use that task as srcDir
. That way you get all the output files as source files and the task dependency where necessary automatically.
And maybe some nits:
• like using layout.buildDirectory
I'd also use layout.projectDirectory
is there anything a bit more...elegantSure, make the maintainer of that task or extension adopt the new Gradle ways, so that it is not a
File
or `String`but a RegularFileProperty
, then what you set it to is layout.buildDirectory.dir("generated/sources/openApi")
.
If they are not following best practices, you have to pay with a bit uglier usage.
Is there anything I should do differentlyYou can apply the
idea
plugin and add the directory as generated source dir, then it is marked appropriately in IJ.
idea {
module {
generatedSourceDirs.add(...)
}
}
Mike Wacker
12/17/2024, 2:02 AMopenApiGenerate
, the srcDir
should depend on that task now. OpenAPI's Github documentation for the Gradle plugin suggests using dependsOn
to build the generated sources, which is what led me down the wrong path.
• For openApiValidate
, is using dependsOn
OK there to add the openApiValidate
task as a dependency of the check
task?
• It looks like there's an existing issue about getting inputSpec
and outputDir
to accept a file; I've added a comment to that.
Hopefully this looks right (it also uses layout.projectDirectory()
). Since everything still works with these changes, I assume that openApiGenerate
is declaring its outputs properly behind-the-scenes:
plugins {
`java-library`
id("org.openapi.generator") version "7.10.0"
}
// Create a task to validate the OpenAPI YAML.
openApiValidate {
inputSpec = layout.projectDirectory.file("src/main/resources/api.yaml").asFile.absolutePath
}
// Check that the Open API YAML is valid.
tasks.named("check") {
dependsOn("openApiValidate")
}
// Create a task to generate the JAX-RS API from the OpenAPI YAML.
openApiGenerate {
generatorName = "jaxrs-spec"
inputSpec = layout.projectDirectory.file("src/main/resources/api.yaml").asFile.absolutePath
outputDir = layout.buildDirectory.dir("generated/sources/openApi").get().asFile.absolutePath
// Generate only the apis and models. Don't generate the invokers or other supporting files.
apiPackage = "org.example.api"
modelPackage = "org.example.api"
globalProperties = mapOf(
"apis" to "",
"models" to "",
)
// Generate an async interface for the apis. Use builders for the models.
configOptions = mapOf(
"generateBuilders" to "true",
"hideGenerationTimestamp" to "true",
"interfaceOnly" to "true",
"sourceFolder" to "java/main",
"supportAsync" to "true",
"useJakartaEe" to "true",
"useSwaggerAnnotations" to "false",
"useTags" to "true",
)
}
// Compile the JAX-RS API that is generated from the OpenAPI YAML.
sourceSets {
main {
java {
srcDir(tasks.openApiGenerate)
}
}
}
repositories {
mavenCentral()
}
dependencies {
api(libs.jackson.annotations)
api(libs.jakartaAnnotation.api)
api(libs.jakartaValidation.api)
api(libs.jaxRs.api)
}
Vampire
12/17/2024, 11:32 AMOpenAPI's Github documentation for the Gradle plugin suggests usingWell, the internet is full of bad practice advices. 😞to build the generated sourcesdependsOn
• For, is usingopenApiValidate
OK there to add thedependsOn
task as a dependency of theopenApiValidate
task?check
check
is a lifecycle task.
It does not have actions, its purpose is to depend on other tasks as kind of "phase".
So yes, as I said, if the left-hand side is a lifecycle task like check
, an explicit dependsOn
is fine.
At least if the intention is exactly like here, "if check
is invoked, also do this checker task".
classes
is also a lifecycle task, but making classes
depend on the generation task would not be good as the intention is a different one.
• It looks like there's an existing issue about gettingActually, as it is aandinputSpec
to accept a file; I've added a comment to that.outputDir
Property<String>
not String
, you should not call get()
.
Calling get()
at configuration time always introduces race conditions.
If you for example get()
the build directory and then later change the build directory to something else you will have the wrong value used. It should be something like outputDir = layout.buildDirectory.dir("generated/sources/openApi").map { it.asFile.absolutePath }
.Mike Wacker
12/18/2024, 8:22 AMget
vs. map
. I've updated the code:
• I've now shoved all this logic into a buildSrc
plugin.
• I no longer add the validate task as a dep of the check
task, since the generate tasks also do validation; check
already runs compileJava
and the generate tasks (as transitive deps of test
).
• I'm now generating server and client code in two generate tasks. I gave each one a separate directory in `build/generated`; I vaguely recall that two tasks shouldn't share an output dir.
import org.gradle.accessors.dm.LibrariesForLibs
plugins {
`java-library`
id("org.openapi.generator")
}
// Create the extension and some path utilities.
interface OpenApiJavaExtension {
val packageName: Property<String>
val inputSpec: RegularFileProperty
val inputSpecPath: Provider<String>
get() = inputSpec.map { it.asFile.absolutePath }
}
val extension = extensions.create<OpenApiJavaExtension>("openApiJava")
extension.packageName.convention("")
extension.inputSpec.convention(layout.projectDirectory.file("src/main/resources/api.yaml"))
afterEvaluate {
if (extension.packageName.get().isEmpty()) {
throw GradleException("openApiJava.packageName must be set")
}
}
fun buildDirPath(relativePath: String): Provider<String> =
layout.buildDirectory.dir(relativePath).map { it.asFile.absolutePath }
// Create a task to validate the OpenAPI YAML.
tasks.register<org.openapitools.generator.gradle.plugin.tasks.ValidateTask>("openApiJavaValidate") {
group = "OpenAPI Java"
description = "Validates the OpenAPI YAML."
inputSpec = extension.inputSpecPath
}
// Create a task to generate JAX-RS server interfaces from the Open API YAML.
val commonGenerateConfigOptions = mapOf(
"dateLibrary" to "java8",
"generateBuilders" to "true",
"hideGenerationTimestamp" to "true",
"openApiNullable" to "false",
"sourceFolder" to "java/main",
"useJakartaEe" to "true",
)
tasks.register<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("openApiJavaGenerateServer") {
group = "OpenAPI Java"
description = "Generates JAX-RS server interfaces from the OpenAPI YAML."
logging.captureStandardOutput(<http://LogLevel.INFO|LogLevel.INFO>)
generatorName = "jaxrs-spec"
inputSpec = extension.inputSpecPath
outputDir = buildDirPath("generated/sources/openApiServer")
// Generate only the apis and models. Don't generate the invokers or other supporting files.
val apiPackageName = extension.packageName.map { "$it.api" }
apiPackage = apiPackageName
modelPackage = apiPackageName
globalProperties = mapOf(
"apis" to "",
"models" to "",
)
// Generate async interfaces for the apis.
configOptions = commonGenerateConfigOptions + mapOf(
"interfaceOnly" to "true",
"supportAsync" to "true",
"useSwaggerAnnotations" to "false",
"useTags" to "true",
)
}
// Create a task to generate Retrofit clients from the Open API YAML.
tasks.register<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("openApiJavaGenerateClient") {
group = "OpenAPI Java"
description = "Generates Retrofit clients from the OpenAPI YAML."
logging.captureStandardOutput(<http://LogLevel.INFO|LogLevel.INFO>)
generatorName = "java"
inputSpec = extension.inputSpecPath
outputDir = buildDirPath("generated/sources/openApiClient")
// Generate everything for main. Don't generate test.
val clientPackageName = extension.packageName.map { "$it.client" }
apiPackage = clientPackageName
modelPackage = clientPackageName
invokerPackage = clientPackageName.map { "$it.retrofit" }
globalProperties = mapOf(
"apiTests" to "false",
"modelTests" to "false",
)
// Generate Retrofit clients using Jackson.
configOptions = commonGenerateConfigOptions + mapOf(
"library" to "retrofit2",
"serializationLibrary" to "jackson",
)
}
// Compile the Java sources that are generated from the OpenAPI YAML.
sourceSets {
main {
java {
srcDir(tasks.named("openApiJavaGenerateServer"))
srcDir(tasks.named("openApiJavaGenerateClient"))
}
}
}
repositories {
mavenCentral()
}
val libs = the<LibrariesForLibs>() // version catalog workaround
dependencies {
api(libs.apacheOauth.client)
api(libs.jackson.annotations)
api(libs.jackson.databind)
api(libs.jakartaAnnotation.api)
api(libs.jakartaValidation.api)
api(libs.jaxRs.api)
api(libs.okhttp.okhttp)
api(libs.retrofit.retrofit)
implementation(libs.jackson.datatypeJsr310)
implementation(libs.retrofit.converterJackson)
implementation(libs.retrofit.converterScalars)
}
The only thing I can't figure is how to disable the Java compilation notes (notes, not warnings):
> Task :api:compileJava
Note: /home/mike/IdeaProjects/sandbox-dropwizard/api/build/generated/sources/openApiClient/java/main/org/example/client/retrofit/auth/OAuthOkHttpClient.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
I thought that adding options.isDeprecation = false
to the tasks of type JavaCompile
would fix it, but apparently not.Vampire
12/18/2024, 8:36 AMI vaguely recall that two tasks shouldn't share an output dir.Yes, never, or bad things happen. You should know this by heart, not only vaguely. 😄
I thought that addingNo,to the tasks of typeoptions.isDeprecation = false
would fix it, but apparently not.JavaCompile
options.isDeprecation = true
is like using -Xlint:deprecation
.
false
is the default.
If you want to suppress those notes, you need to check whether the compiler supports turning them off and then giving it the necessary argument.
I don't think this is built-in as option in Gradle.Mike Wacker
12/18/2024, 10:31 PMtasks.withType<JavaCompile> {
logging.captureStandardError(<http://LogLevel.INFO|LogLevel.INFO>) // suppress compilation notes
}
I double-checked this by removing an api
dep to introduce compilation errors, but it does seem like suppressing INFO logs won't remove anything that you couldn't already learn from WARN and ERROR logs.