From e65ba3b1622133e87db49737cce4f665d96d4c9a Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Mon, 21 Mar 2022 20:02:43 -0700 Subject: [PATCH] Make the plugin output directory configurable Make outputDir configurable, so that users can have full control over the output path. Additionally, the GenerateJavaTask lazy by converting inputs to gradle Properties. See: https://docs.gradle.org/current/userguide/lazy_configuration.html --- .../dgs/codegen/gradle/CodegenPlugin.kt | 3 +- .../dgs/codegen/gradle/GenerateJavaTask.kt | 227 +++++++++++------- .../graphql/dgs/CodegenGradlePluginTest.kt | 62 ++++- .../test-project/build_with_output_dir.gradle | 34 +++ 4 files changed, 220 insertions(+), 106 deletions(-) create mode 100644 graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_output_dir.gradle diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt index 9bcfdd879..b97d66730 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/CodegenPlugin.kt @@ -44,7 +44,8 @@ class CodegenPlugin : Plugin { val javaConvention = project.convention.getPlugin(JavaPluginConvention::class.java) val sourceSets = javaConvention.sourceSets val mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME) - val outputDir = generateJavaTaskProvider.map(GenerateJavaTask::getOutputDir) + val outputDir = generateJavaTaskProvider.flatMap(GenerateJavaTask::outputDir) + mainSourceSet.java.srcDirs(project.files(outputDir).builtBy(generateJavaTaskProvider)) project.afterEvaluate { p -> diff --git a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt index c80372fd9..ea75ea6e2 100644 --- a/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt +++ b/graphql-dgs-codegen-gradle/src/main/kotlin/com/netflix/graphql/dgs/codegen/gradle/GenerateJavaTask.kt @@ -22,6 +22,14 @@ import com.netflix.graphql.dgs.codegen.CodeGen import com.netflix.graphql.dgs.codegen.CodeGenConfig import com.netflix.graphql.dgs.codegen.Language import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.ProjectLayout +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.ProviderFactory +import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles @@ -30,133 +38,166 @@ import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper -import java.io.File import java.nio.file.Paths -import java.util.* +import java.util.Locale +import javax.inject.Inject @CacheableTask -open class GenerateJavaTask : DefaultTask() { - @Input - var generatedSourcesDir: String = project.buildDir.absolutePath +abstract class GenerateJavaTask @Inject constructor( + projectLayout: ProjectLayout, + providerFactory: ProviderFactory, + objectFactory: ObjectFactory +) : DefaultTask() { + @get:Input + val generatedSourcesDir: Property = objectFactory.property(String::class.java) + .convention(projectLayout.buildDirectory.map { it.asFile.absolutePath }) @PathSensitive(PathSensitivity.RELATIVE) - @InputFiles - var schemaPaths = mutableListOf("${project.projectDir}/src/main/resources/schema") - - @Input - var packageName = "com.netflix.dgs.codegen.generated" - - @Input - var subPackageNameClient = "client" - - @Input - var subPackageNameDatafetchers = "datafetchers" - - @Input - var subPackageNameTypes = "types" - - private val hasKotlinPluginWrapperClass = try { - this.javaClass.classLoader.loadClass("org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper") - true - } catch (ex: Exception) { - false - } - - @Input - var language = if (hasKotlinPluginWrapperClass && project.plugins.hasPlugin(KotlinPluginWrapper::class.java)) "KOTLIN" else "JAVA" + @get:InputFiles + val schemaPaths: ListProperty = objectFactory.listProperty(Any::class.java) + .convention(listOf(projectLayout.projectDirectory.dir("src/main/resources/schema").toString())) + + @get:Input + val packageName: Property = objectFactory.property(String::class.java) + .convention("com.netflix.dgs.codegen.generated") + + @get:Input + val subPackageNameClient: Property = objectFactory.property(String::class.java) + .convention("client") + + @get:Input + val subPackageNameDatafetchers: Property = objectFactory.property(String::class.java) + .convention("datafetchers") + + @get:Input + val subPackageNameTypes: Property = objectFactory.property(String::class.java) + .convention("types") + + @get:Input + val language: Property = objectFactory.property(String::class.java) + .convention( + providerFactory.provider { + val hasKotlinPluginWrapperClass = try { + this.javaClass.classLoader.loadClass("org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper") + true + } catch (ex: Exception) { + false + } + if (hasKotlinPluginWrapperClass && project.plugins.hasPlugin(KotlinPluginWrapper::class.java)) "KOTLIN" else "JAVA" + } + ) - @Input - var typeMapping = mutableMapOf() + @get:Input + val typeMapping: MapProperty = objectFactory.mapProperty(String::class.java, String::class.java) - @Input - var generateBoxedTypes = false + @get:Input + val generateBoxedTypes: Property = objectFactory.property(Boolean::class.java) + .convention(false) - @Input - var generateClient = false + @get:Input + val generateClient: Property = objectFactory.property(Boolean::class.java) + .convention(false) - @Input - var generateDataTypes = true + @get:Input + val generateDataTypes: Property = objectFactory.property(Boolean::class.java) + .convention(true) - @Input - var generateInterfaces = false + @get:Input + val generateInterfaces: Property = objectFactory.property(Boolean::class.java) + .convention(false) - @Input - var generateInterfaceSetters = true + @get:Input + val generateInterfaceSetters: Property = objectFactory.property(Boolean::class.java) + .convention(true) - @OutputDirectory - fun getOutputDir(): File { - return Paths.get("$generatedSourcesDir/generated/sources/dgs-codegen").toFile() - } + @get:OutputDirectory + val outputDir: DirectoryProperty = objectFactory.directoryProperty() + .convention( + generatedSourcesDir.flatMap { baseDir -> + projectLayout.dir(providerFactory.provider { project.file("$baseDir/generated/sources/dgs-codegen") }) + } + ) - @OutputDirectory - fun getExampleOutputDir(): File { - return Paths.get("$generatedSourcesDir/generated/sources/dgs-codegen-generated-examples").toFile() - } + @get:OutputDirectory + val exampleOutputDir: DirectoryProperty = objectFactory.directoryProperty() + .convention( + generatedSourcesDir.flatMap { baseDir -> + projectLayout.dir(providerFactory.provider { project.file("$baseDir/generated/sources/dgs-codegen-generated-examples") }) + } + ) - @Input - var includeQueries = mutableListOf() + @get:Input + val includeQueries: SetProperty = objectFactory.setProperty(String::class.java) - @Input - var includeMutations = mutableListOf() + @get:Input + val includeMutations: SetProperty = objectFactory.setProperty(String::class.java) - @Input - var includeSubscriptions = mutableListOf() + @get:Input + val includeSubscriptions: SetProperty = objectFactory.setProperty(String::class.java) - @Input - var skipEntityQueries = false + @get:Input + val skipEntityQueries: Property = objectFactory.property(Boolean::class.java) + .convention(false) - @Input - var shortProjectionNames = false + @get:Input + val shortProjectionNames: Property = objectFactory.property(Boolean::class.java) + .convention(false) - @Input - var omitNullInputFields = false + @get:Input + val omitNullInputFields: Property = objectFactory.property(Boolean::class.java) + .convention(false) - @Input - var maxProjectionDepth = 10 + @get:Input + val maxProjectionDepth: Property = objectFactory.property(Int::class.javaObjectType) + .convention(10) - @Input - var kotlinAllFieldsOptional = false + @get:Input + val kotlinAllFieldsOptional: Property = objectFactory.property(Boolean::class.java) + .convention(false) - @Input - var snakeCaseConstantNames = false + @get:Input + val snakeCaseConstantNames: Property = objectFactory.property(Boolean::class.java) + .convention(false) @TaskAction fun generate() { - val schemaPaths = schemaPaths.map { Paths.get(it.toString()).toFile() }.sorted().toSet() - schemaPaths.filter { !it.exists() }.forEach { - logger.warn("Schema location ${it.absolutePath} does not exist") + val schemaPaths = schemaPaths.get().asSequence() + .map { Paths.get(it.toString()).toFile() } + .toSortedSet() + schemaPaths.asSequence().filter { !it.exists() }.forEach { + logger.warn("Schema location {} does not exist", it.absolutePath) } logger.info("Processing schema files:") schemaPaths.forEach { - logger.info("Processing $it") + logger.info("Processing {}", it) } val config = CodeGenConfig( schemas = emptySet(), schemaFiles = schemaPaths, - outputDir = getOutputDir().toPath(), - examplesOutputDir = getExampleOutputDir().toPath(), + outputDir = outputDir.get().asFile.toPath(), + examplesOutputDir = exampleOutputDir.get().asFile.toPath(), writeToFiles = true, - packageName = packageName, - subPackageNameClient = subPackageNameClient, - subPackageNameDatafetchers = subPackageNameDatafetchers, - subPackageNameTypes = subPackageNameTypes, - language = Language.valueOf(language.uppercase(Locale.getDefault())), - generateBoxedTypes = generateBoxedTypes, - generateClientApi = generateClient, - generateInterfaces = generateInterfaces, - generateInterfaceSetters = generateInterfaceSetters, - typeMapping = typeMapping, - includeQueries = includeQueries.toSet(), - includeMutations = includeMutations.toSet(), - includeSubscriptions = includeSubscriptions.toSet(), - skipEntityQueries = skipEntityQueries, - shortProjectionNames = shortProjectionNames, - generateDataTypes = generateDataTypes, - omitNullInputFields = omitNullInputFields, - maxProjectionDepth = maxProjectionDepth, - kotlinAllFieldsOptional = kotlinAllFieldsOptional, - snakeCaseConstantNames = snakeCaseConstantNames + packageName = packageName.get(), + subPackageNameClient = subPackageNameClient.get(), + subPackageNameDatafetchers = subPackageNameDatafetchers.get(), + subPackageNameTypes = subPackageNameTypes.get(), + language = Language.valueOf(language.get().uppercase(Locale.getDefault())), + generateBoxedTypes = generateBoxedTypes.get(), + generateClientApi = generateClient.get(), + generateInterfaces = generateInterfaces.get(), + generateInterfaceSetters = generateInterfaceSetters.get(), + typeMapping = typeMapping.get(), + includeQueries = includeQueries.get(), + includeMutations = includeMutations.get(), + includeSubscriptions = includeSubscriptions.get(), + skipEntityQueries = skipEntityQueries.get(), + shortProjectionNames = shortProjectionNames.get(), + generateDataTypes = generateDataTypes.get(), + omitNullInputFields = omitNullInputFields.get(), + maxProjectionDepth = maxProjectionDepth.get(), + kotlinAllFieldsOptional = kotlinAllFieldsOptional.get(), + snakeCaseConstantNames = snakeCaseConstantNames.get() ) logger.info("Codegen config: {}", config) diff --git a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt index bd6763255..6b6d94830 100644 --- a/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt +++ b/graphql-dgs-codegen-gradle/src/test/kotlin/com/netflix/graphql/dgs/CodegenGradlePluginTest.kt @@ -23,19 +23,32 @@ package com.netflix.graphql.dgs import org.assertj.core.api.Assertions.assertThat import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import java.io.File +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.div /** * A simple unit test for the 'com.netflix.graphql.dgs.greeting' plugin. */ class CodegenGradlePluginTest { + @TempDir + lateinit var projectDir: Path + + @BeforeEach + fun setUp() { + val projectResourcePath = Paths.get("src", "test", "resources", "test-project") + projectResourcePath.toFile().copyRecursively(target = projectDir.toFile()) + } + @Test fun taskRegisteredSuccessfully() { // get a list of Gradle tasks val result = GradleRunner.create() - .withProjectDir(File("src/test/resources/test-project/")) + .withProjectDir(projectDir.toFile()) .withPluginClasspath() .withArguments( "-c", "smoke_test_settings.gradle", @@ -51,7 +64,7 @@ class CodegenGradlePluginTest { fun taskDependenciesRegisteredSuccessfully() { // get a list of Gradle tasks val result = GradleRunner.create() - .withProjectDir(File("src/test/resources/test-project/")) + .withProjectDir(projectDir.toFile()) .withPluginClasspath() .withArguments( "-c", "smoke_test_settings.gradle", @@ -69,7 +82,7 @@ class CodegenGradlePluginTest { fun sourcesGenerated() { // build a project val result = GradleRunner.create() - .withProjectDir(File("src/test/resources/test-project/")) + .withProjectDir(projectDir.toFile()) .withPluginClasspath() .withArguments( "--stacktrace", @@ -83,14 +96,15 @@ class CodegenGradlePluginTest { // Verify the result assertThat(result.task(":build")).extracting { it?.outcome }.isEqualTo(SUCCESS) // Verify that POJOs are generated in the configured directory - assertThat(File(EXPECTED_PATH + "Result.java").exists()).isTrue + + assertThat(projectDir / EXPECTED_PATH / "Result.java").exists() } @Test fun sourcesGenerated_UsingDefaultPath() { // build a project val result = GradleRunner.create() - .withProjectDir(File("src/test/resources/test-project/")) + .withProjectDir(projectDir.toFile()) .withPluginClasspath() .withArguments( "--stacktrace", @@ -105,14 +119,36 @@ class CodegenGradlePluginTest { // Verify the result assertThat(result.task(":build")).extracting { it?.outcome }.isEqualTo(SUCCESS) // Verify that POJOs are generated in the configured directory - assertThat(File(EXPECTED_DEFAULT_PATH + "Result.java").exists()).isTrue + assertThat(projectDir / EXPECTED_DEFAULT_PATH / "Result.java").exists() + assertThat(projectDir / EXPECTED_DEFAULT_PATH / "Result.java").exists() + } + + @Test + fun sourcesGenerated_UsingOverridenOutputDir() { + // build a project + val result = GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withPluginClasspath() + .withArguments( + "--stacktrace", + "-b", "build_with_output_dir.gradle", + "clean", + "build" + ).forwardOutput() + .withDebug(true) + .build() + + // Verify the result + assertThat(result.task(":build")).extracting { it?.outcome }.isEqualTo(SUCCESS) + // Verify that POJOs are generated in the configured directory + assertThat(projectDir / BUILD_DIR / "custom-generated" / SOURCES_PATH / "Result.java").exists() } @Test fun sourcesGenerated_OmitNullInputFields() { // build a project val result = GradleRunner.create() - .withProjectDir(File("src/test/resources/test-project/")) + .withProjectDir(projectDir.toFile()) .withPluginClasspath() .withArguments( "--stacktrace", @@ -128,12 +164,14 @@ class CodegenGradlePluginTest { // Verify the result assertThat(result.task(":build")).extracting { it?.outcome }.isEqualTo(SUCCESS) // Verify that POJOs are generated in the configured directory - assertThat(File(EXPECTED_DEFAULT_PATH + "Result.java").exists()).isTrue - assertThat(File(EXPECTED_DEFAULT_PATH + "Filter.java").exists()).isTrue + assertThat(projectDir / EXPECTED_DEFAULT_PATH / "Result.java").exists() + assertThat(projectDir / EXPECTED_DEFAULT_PATH / "Filter.java").exists() } companion object { - const val EXPECTED_PATH = "src/test/resources/test-project/build/graphql/generated/sources/dgs-codegen/com/netflix/testproject/graphql/types/" - const val EXPECTED_DEFAULT_PATH = "src/test/resources/test-project/build/generated/sources/dgs-codegen/com/netflix/testproject/graphql/types/" + private val BUILD_DIR = Paths.get("build") + private val SOURCES_PATH = Paths.get("com", "netflix", "testproject", "graphql", "types") + private val EXPECTED_PATH = BUILD_DIR / "graphql" / "generated" / "sources" / "dgs-codegen" / SOURCES_PATH + private val EXPECTED_DEFAULT_PATH = BUILD_DIR / "generated" / "sources" / "dgs-codegen" / SOURCES_PATH } } diff --git a/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_output_dir.gradle b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_output_dir.gradle new file mode 100644 index 000000000..c3d4414d4 --- /dev/null +++ b/graphql-dgs-codegen-gradle/src/test/resources/test-project/build_with_output_dir.gradle @@ -0,0 +1,34 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +plugins { + id 'java' + id 'com.netflix.dgs.codegen' +} + +configurations { + // injected by Gradle Runner through test configuration, see CodegenGradlePluginTest + CodeGenConfiguration.exclude group: "com.netflix.graphql.dgs.codegen", module: "graphql-dgs-codegen-core" +} + +generateJava { + packageName = 'com.netflix.testproject.graphql' + outputDir = file("${buildDir}/custom-generated/") +} + +codegen.clientCoreConventionsEnabled = false