diff --git a/modules/openapi-generator-gradle-plugin/README.adoc b/modules/openapi-generator-gradle-plugin/README.adoc index e6e7daa7c441..f2d39b929c6e 100644 --- a/modules/openapi-generator-gradle-plugin/README.adoc +++ b/modules/openapi-generator-gradle-plugin/README.adoc @@ -63,15 +63,16 @@ task validateSpecs(dependsOn: ['validateGoodSpec', 'validateBadSpec']) [NOTE] ==== -The tasks support Gradle Up-To-Date checking and Gradle Cache. Enable caching globally by setting `org.gradle.caching=true` in the `gradle.settings` -file or by passing the command line property `--build-cache` when executing on the command line. +**Modern Gradle Support:** This plugin fully supports Gradle's **Configuration Cache**, **Build Cache**, and **Lazy Configuration (Provider API)**. -Disable up-to-date checks and caching by setting the following property when using the extension: +Enable caching globally by setting `org.gradle.caching=true` and `org.gradle.configuration-cache=true` in your `gradle.properties` file, or by passing `--build-cache` and `--configuration-cache` on the command line. + +If you need to disable up-to-date checks and caching for a specific task, you can do so like this: .Disable caching for extension [source,groovy] ---- -tasks.withType(org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { +tasks.withType(org.openapitools.generator.gradle.plugin.tasks.GenerateTask).configureEach { outputs.upToDateWhen { false } outputs.cacheIf { false } } @@ -130,312 +131,312 @@ apply plugin: 'org.openapi.generator' |Key |Data Type |Default |Description |verbose -|Boolean +|Boolean / Provider |false |The verbosity of generation |validateSpec -|Boolean +|Boolean / Provider |true |Whether or not we should validate the input spec before generation. Invalid specs result in an error. |generatorName -|String +|String / Provider |None |The name of the generator which will handle codegen. |outputDir -|String +|String / Provider |None |The output target directory into which code will be generated. |inputSpec -|String +|String / Provider |None -|The Open API 2.0/3.x specification location. +|The Open API 2.0/3.x specification location. This acts as a smart router: if a local path is provided, it utilizes Gradle's Lazy File API for caching. If a URL/URI (e.g., `http://`, `jar:`) is provided, it automatically routes to `remoteInputSpec`. |inputSpecRootDirectory -|String +|String / Provider |None |Local root folder with spec file(s) |mergedFileName -|String +|String / Provider |None |Name of the file that will contain all merged specs |remoteInputSpec -|String +|String / Provider |None -|The remote Open API 2.0/3.x specification URL location. +|The remote Open API 2.0/3.x specification URL location. (Note: Using remote specs may result in stale build caches if the remote content changes without the URL changing). |templateDir -|String +|String / Provider |None |The template directory holding a custom template. |templateResourcePath -|String +|String / Provider |None |Directory with mustache templates via resource path. This option will overwrite any option defined in `templateDir` |auth -|String +|String / Provider |None |Adds authorization headers when fetching the OpenAPI definitions remotely. Pass in a URL-encoded string of name:header with a comma separating multiple values. |globalProperties -|Map(String,String) +|Map / Provider |None |Sets specified global properties. |configFile -|String +|String / Provider |None |Path to json configuration file. See OpenAPI Generator readme for structure details. |skipOverwrite -|Boolean +|Boolean / Provider |false |Specifies if the existing files should be overwritten during the generation. |packageName -|String +|String / Provider |(generator specific) |Package for generated classes (where supported). |apiPackage -|String +|String / Provider |(generator specific) |Package for generated api classes. |modelPackage -|String +|String / Provider |(generator specific) |Package for generated model classes. |modelNamePrefix -|String +|String / Provider |None |Prefix that will be prepended to all model names. |modelNameSuffix -|String +|String / Provider |None |Suffix that will be appended to all model names. |apiNameSuffix -|String +|String / Provider |None |Suffix that will be appended to all api names. |instantiationTypes -|Map(String,String) +|Map / Provider |None |Sets instantiation type mappings. |typeMappings -|Map(String,String) +|Map / Provider |None |Sets mappings between OpenAPI spec types and generated code types in the format of OpenAPIType=generatedType,OpenAPIType=generatedType. For example: `array=List,map=Map,string=String`. You can also have multiple occurrences of this option. To map a specified format, use type+format, e.g. string+password=EncryptedString will map `type: string, format: password` to `EncryptedString`. |schemaMappings -|Map(String,String) +|Map / Provider |None |specifies mappings between the schema and the new name in the format of schema_a=Cat,schema_b=Bird. https://openapi-generator.tech/docs/customization/#schema-mapping |nameMappings -|Map(String,String) +|Map / Provider |None |specifies mappings between the property name and the new name in the format of property_a=firstProperty,property_b=secondProperty. https://openapi-generator.tech/docs/customization/#name-mapping |modelNameMappings -|Map(String,String) +|Map / Provider |None |specifies mappings between the model name and the new name in the format of model_a=FirstModel,property_b=SecondModel. https://openapi-generator.tech/docs/customization/#name-mapping |parameterNameMappings -|Map(String,String) +|Map / Provider |None |specifies mappings between the parameter name and the new name in the format of parameter_a=firstParameter,parameter_b=secondParameter. https://openapi-generator.tech/docs/customization/#name-mapping |inlineSchemaNameMappings -|Map(String,String) +|Map / Provider |None |specifies mappings between the inline schema name and the new name in the format of inline_object_2=Cat,inline_object_5=Bird. |inlineSchemaOptions -|Map(String,String) +|Map / Provider |None |specifies the options used when handling inline schema in inline model resolver |additionalProperties -|Map(String,Any) +|Map / Provider |None |Sets additional properties that can be referenced by the mustache templates. |serverVariables -|Map(String,String) +|Map / Provider |None |Sets server variable for server URL template substitution, in the format of name=value,name=value. You can also have multiple occurrences of this option. |languageSpecificPrimitives -|List(String) +|List / Provider |None |Specifies additional language specific primitive types in the format of type1,type2,type3,type3. For example: String,boolean,Boolean,Double. |importMappings -|Map(String,String) +|Map / Provider |None |Specifies mappings between a given class and the import that should be used for that class. |invokerPackage -|String +|String / Provider |None |Root package for generated code. |groupId -|String +|String / Provider |None |GroupId in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators. |id -|String +|String / Provider |None |ArtifactId in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators. |version -|String +|String / Provider |None |Artifact version in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators. |library -|String +|String / Provider |None |Reference the library template (sub-template) of a generator. |gitHost -|String +|String / Provider |github.com |Git user ID, e.g. gitlab.com. |gitUserId -|String +|String / Provider |None |Git user ID, e.g. openapitools. |gitRepoId -|String +|String / Provider |None |Git repo ID, e.g. openapi-generator. |releaseNote -|String +|String / Provider |'Minor update' |Release note. |httpUserAgent -|String +|String / Provider |None |HTTP user agent, e.g. codegen_csharp_api_client. Generator default is 'OpenAPI-Generator/{packageVersion}/{language}', but may be generator-specific. |reservedWordsMappings -|Map(String,String) +|Map / Provider |None |Specifies how a reserved name should be escaped to. Otherwise, the default _ is used. |ignoreFileOverride -|String +|String / Provider |None |Specifies an override location for the .openapi-generator-ignore file. Most useful on initial generation. |removeOperationIdPrefix -|Boolean +|Boolean / Provider |false |Remove prefix of operationId, e.g. config_getId => getId. |skipOperationExample -|Boolean +|Boolean / Provider |false |Skip examples defined in the operation |apiFilesConstrainedTo -|List(String) +|List / Provider |None |Defines which API-related files should be generated. This allows you to create a subset of generated files (or none at all). See Note Below. |modelFilesConstrainedTo -|List(String) +|List / Provider |None |Defines which model-related files should be generated. This allows you to create a subset of generated files (or none at all). See Note Below. |supportingFilesConstrainedTo -|List(String) +|List / Provider |None |Defines which supporting files should be generated. This allows you to create a subset of generated files (or none at all). See Note Below. |generateModelTests -|Boolean +|Boolean / Provider |true |Defines whether or not model-related _test_ files should be generated. |generateModelDocumentation -|Boolean +|Boolean / Provider |true |Defines whether or not model-related _documentation_ files should be generated. |generateApiTests -|Boolean +|Boolean / Provider |true |Defines whether or not api-related _test_ files should be generated. |generateApiDocumentation -|Boolean +|Boolean / Provider |true |Defines whether or not api-related _documentation_ files should be generated. |configOptions -|Map(String,String) +|Map / Provider |None |A map of options specific to a generator. To see the full list of generator-specified parameters, please refer to https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators.md[generators docs]. Note that any config options from a generator specific document may go here, and some generators may duplicate other options which are siblings to `configOptions`. |logToStderr -|Boolean +|Boolean / Provider |false |To write all log messages (not just errors) to STDOUT |enablePostProcessFile -|Boolean +|Boolean / Provider |false |To enable the file post-processing hook. This enables executing an external post-processor (usually a linter program). This only enables the post-processor. To define the post-processing command, define an environment variable such as LANG_POST_PROCESS_FILE (e.g. GO_POST_PROCESS_FILE, SCALA_POST_PROCESS_FILE). Please open an issue if your target generator does not support this functionality. |skipValidateSpec -|Boolean +|Boolean / Provider |false |To skip spec validation. When true, we will skip the default behavior of validating a spec before generation. |openapiNormalizer -|Map(String,String) +|Map / Provider |None |specifies the rules to be enabled in OpenAPI normalizer in the form of RULE_1=true,RULE_2=original. |generateAliasAsModel -|Boolean +|Boolean / Provider |false |To generate alias (array, list, map) as model. When false, top-level objects defined as array, list, or map will result in those definitions generated as top-level Array-of-items, List-of-items, Map-of-items definitions. When true, A model representation either containing or extending the array,list,map (depending on specific generator implementation) will be generated. |engine -|String +|String / Provider |mustache |Templating engine: "mustache" (default) or "handlebars" (beta) |cleanupOutput -|Boolean +|Boolean / Provider |false |Defines whether the output directory should be cleaned up before generating the output. |dryRun -|Boolean +|Boolean / Provider |false |Defines whether the generator should run in dry-run mode. In dry-run mode no files are written and a summary about file states is output. @@ -455,22 +456,22 @@ When configuring `globalProperties` in order to perform selective generation you [source,groovy] ---- openApiGenerate { - // other settings omitted - globalProperties.set([ - modelDocs: "false", - apis: "false" - ]) +// other settings omitted +globalProperties.set([ +modelDocs: "false", +apis: "false" +]) } ---- When enabling generation of only specific parts you either have to provide CSV list of what you particularly are generating or provide an empty string `""` to generate everything. If you provide `"true"` it will be treated as a specific name of model or api you want to generate. [source,groovy] ---- openApiGenerate { - // other settings omitted - globalProperties.set([ - apis: "", - models: "User:Pet" - ]) +// other settings omitted +globalProperties.set([ +apis: "", +models: "User:Pet" +]) } ---- ==== @@ -482,9 +483,9 @@ openApiGenerate { |Key |Data Type |Default |Description |inputSpec -|String +|String / Provider |None -|The input specification to validate. Supports all formats supported by the Parser. +|The input specification to validate. Acts as a smart router, supporting local files (with full Gradle caching) or remote URLs/URIs. |recommend |Boolean @@ -742,6 +743,35 @@ you need a task reference or instance. One way to do this is to access it as `ta You can run `gradle tasks --debug` to see this registration. ==== +=== Advanced: Task Wiring and Lazy Configuration + +The OpenAPI Generator plugin fully supports Gradle's Provider API and Lazy Configuration. This means that if your OpenAPI spec is generated or downloaded by another Gradle task, you **do not** need to use `dependsOn` or hardcode paths in the `buildDir`. + +Instead of passing a static string path, simply pass the output `Provider` from your producer task directly into the generator's properties. Gradle will automatically build the task dependency graph and execute them in the correct order. + +.Implicit Task Wiring (Kotlin DSL) +[source,kotlin] +---- +// 1. A task that produces a spec file +val downloadSpec by tasks.registering(DownloadTask::class) { + sourceUrl.set("https://api.mycompany.com/openapi.yaml") + outputFile.set(layout.buildDirectory.file("downloaded-spec.yaml")) +} + +// 2. The generator task +openApiGenerate { + generatorName.set("kotlin") + + // Wire the output of the download task directly to the input of the generator. + // Gradle automatically knows `openApiGenerate` depends on `downloadSpec`. + inputSpec.set(downloadSpec.flatMap { it.outputFile }) + + outputDir.set(layout.buildDirectory.dir("generated-code")) +} +---- + +*Note: Standard string assignments (e.g., `inputSpec = "path/to/file.yaml"`) are still fully supported via bridge methods for static files checked into your repository.* + == Troubleshooting === Android Studio diff --git a/modules/openapi-generator-gradle-plugin/samples/local-spec/build.gradle b/modules/openapi-generator-gradle-plugin/samples/local-spec/build.gradle index f21bca232dff..44d0dc10e216 100644 --- a/modules/openapi-generator-gradle-plugin/samples/local-spec/build.gradle +++ b/modules/openapi-generator-gradle-plugin/samples/local-spec/build.gradle @@ -23,19 +23,19 @@ apply plugin: 'org.openapi.generator' openApiMeta { generatorName = "Sample" packageName = "org.openapitools.example" - outputFolder = layout.buildDirectory.dir("meta").get().asFile.toString() + outputFolder = layout.buildDirectory.dir("meta") } openApiValidate { - inputSpec = "$rootDir/petstore-v3.0-invalid.yaml".toString() + inputSpec = layout.projectDirectory.file("petstore-v3.0-invalid.yaml") recommend = true } // Builds a Kotlin client by default. openApiGenerate { generatorName = "kotlin" - inputSpec = "$rootDir/petstore-v3.0.yaml".toString() - outputDir = layout.buildDirectory.dir("kotlin").get().asFile.toString() + inputSpec = layout.projectDirectory.file("petstore-v3.0.yaml") + outputDir = layout.buildDirectory.dir("kotlin") apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -54,11 +54,11 @@ openApiGenerate { enablePostProcessFile = false } -task buildJavaResttemplateSdk(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { +tasks.register('buildJavaResttemplateSdk', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { generatorName = "java" library = "resttemplate" - inputSpec = "$rootDir/petstore-v3.0.yaml".toString() - outputDir = layout.buildDirectory.dir("java-resttemplate-api-client").get().asFile.toString() + inputSpec = layout.projectDirectory.file("petstore-v3.0.yaml") + outputDir = layout.buildDirectory.dir("java-resttemplate-api-client") apiPackage = "com.example.client" invokerPackage = "com.example.invoker" modelPackage = "com.example.cdm" @@ -73,51 +73,51 @@ task buildJavaResttemplateSdk(type: org.openapitools.generator.gradle.plugin.tas enablePostProcessFile = false } -task buildGoSdk(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask){ +tasks.register('buildGoSdk', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { generatorName = "go" - inputSpec = "$rootDir/petstore-v3.0.yaml".toString() + inputSpec = layout.projectDirectory.file("petstore-v3.0.yaml") additionalProperties = [ packageName: "petstore" ] - outputDir = layout.buildDirectory.dir("go").get().asFile.toString() + outputDir = layout.buildDirectory.dir("go") configOptions = [ dateLibrary: "threetenp" ] } -task buildDotnetSdk(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask){ +tasks.register('buildDotnetSdk', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { generatorName = "csharp" - inputSpec = "$rootDir/petstore-v3.0.yaml".toString() + inputSpec = layout.projectDirectory.file("petstore-v3.0.yaml") additionalProperties = [ - packageGuid: "{321C8C3F-0156-40C1-AE42-D59761FB9B6C}", + packageGuid : "{321C8C3F-0156-40C1-AE42-D59761FB9B6C}", useCompareNetObjects: "true" ] - outputDir = layout.buildDirectory.dir("csharp").get().asFile.toString() + outputDir = layout.buildDirectory.dir("csharp") globalProperties = [ models: "", apis : "", ] } -task generateGoWithInvalidSpec(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask){ +tasks.register('generateGoWithInvalidSpec', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { validateSpec = true generatorName = "go" - inputSpec = "$rootDir/petstore-v3.0-invalid.yaml".toString() + inputSpec = layout.projectDirectory.file("petstore-v3.0-invalid.yaml") additionalProperties = [ packageName: "petstore" ] - outputDir = layout.buildDirectory.dir("go").get().asFile.toString() + outputDir = layout.buildDirectory.dir("go") configOptions = [ dateLibrary: "threetenp" ] } -task validateGoodSpec(type: org.openapitools.generator.gradle.plugin.tasks.ValidateTask){ - inputSpec = "$rootDir/petstore-v3.0.yaml".toString() +def validateGoodSpec = tasks.register('validateGoodSpec', org.openapitools.generator.gradle.plugin.tasks.ValidateTask) { + inputSpec = layout.projectDirectory.file("petstore-v3.0.yaml") } -task validateBadSpec(type: org.openapitools.generator.gradle.plugin.tasks.ValidateTask){ - inputSpec = "$rootDir/petstore-v3.0-invalid.yaml".toString() +def validateBadSpec = tasks.register('validateBadSpec', org.openapitools.generator.gradle.plugin.tasks.ValidateTask) { + inputSpec = layout.projectDirectory.file("petstore-v3.0-invalid.yaml") } -task validateSpecs(dependsOn: ['validateGoodSpec', 'validateBadSpec']) +tasks.register('validateSpecs') { dependsOn validateGoodSpec, validateBadSpec } diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt index 1b2d37526516..17d449a004d0 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt @@ -60,7 +60,7 @@ class OpenApiGeneratorPlugin : Plugin { project ) - generate.outputDir.set(project.layout.buildDirectory.dir("generate-resources/main").map { it.asFile.path }) + generate.outputDir.convention(layout.buildDirectory.dir("generate-resources/main")) tasks.apply { register("openApiGenerators", GeneratorsTask::class.java).configure { diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt index 3d5a3489cf01..a03bedd05788 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt @@ -17,18 +17,19 @@ package org.openapitools.generator.gradle.plugin.extensions import org.gradle.api.Project -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.Optional +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty import org.gradle.kotlin.dsl.listProperty import org.gradle.kotlin.dsl.mapProperty import org.gradle.kotlin.dsl.property +import org.openapitools.generator.gradle.plugin.utils.isRemoteUri /** * Gradle project level extension object definition for the `generate` task * * @author Jim Schubert */ -open class OpenApiGeneratorGenerateExtension(project: Project) { +open class OpenApiGeneratorGenerateExtension(private val project: Project) { /** * The verbosity of generation */ @@ -47,7 +48,7 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { /** * The output target directory into which code will be generated. */ - val outputDir = project.objects.property() + val outputDir: DirectoryProperty = project.objects.directoryProperty() /** * The Open API 2.0/3.x specification location. @@ -56,7 +57,7 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { * changes to any $ref referenced files. Use the `inputSpecRootDirectory` property to have Gradle track changes to * an entire directory of spec files. */ - val inputSpec = project.objects.property() + val inputSpec: RegularFileProperty = project.objects.fileProperty() /** * Local root folder with spec files. @@ -64,7 +65,7 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { * By default, a merged spec file will be generated based on the contents of the directory. To disable this, set the * `inputSpecRootDirectorySkipMerge` property. */ - val inputSpecRootDirectory = project.objects.property() + val inputSpecRootDirectory: DirectoryProperty = project.objects.directoryProperty() /** * Skip bundling all spec files into a merged spec file, if true. @@ -81,7 +82,7 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { /** * The template directory holding a custom template. */ - val templateDir = project.objects.property() + val templateDir: DirectoryProperty = project.objects.directoryProperty() /** * The template location (which may be a directory or a classpath location) holding custom templates. @@ -104,7 +105,7 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { * File content should be in a json format { "optionKey":"optionValue", "optionKey1":"optionValue1"...} * Supported options can be different for each language. Run config-help -g {generator name} command for language specific config options. */ - val configFile = project.objects.property() + val configFile: RegularFileProperty = project.objects.fileProperty() /** * Specifies if the existing files should be overwritten during the generation. @@ -167,7 +168,7 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { val languageSpecificPrimitives = project.objects.listProperty() /** - * Specifies .openapi-generator-ignore list in the form of relative/path/to/file1,relative/path/to/file2. For example: README.md,pom.xml. + * Specifies .openapi-generator-ignore list in the form of relative/path/to/file1,relative/path/to/file2. For example: README.md,pom.xml. */ val openapiGeneratorIgnoreList = project.objects.listProperty() @@ -279,7 +280,7 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { /** * Specifies an override location for the .openapi-generator-ignore file. Most useful on initial generation. */ - val ignoreFileOverride = project.objects.property() + val ignoreFileOverride: RegularFileProperty = project.objects.fileProperty() /** * Remove prefix of operationId, e.g. config_getId => getId @@ -413,22 +414,62 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { @Suppress("MemberVisibilityCanBePrivate") fun applyDefaults() { - releaseNote.set("Minor update") - inputSpecRootDirectorySkipMerge.set(false) - modelNamePrefix.set("") - modelNameSuffix.set("") - apiNameSuffix.set("") - generateModelTests.set(true) - generateModelDocumentation.set(true) - generateApiTests.set(true) - generateApiDocumentation.set(true) - configOptions.set(mapOf()) - validateSpec.set(true) - logToStderr.set(false) - enablePostProcessFile.set(false) - skipValidateSpec.set(false) - generateAliasAsModel.set(false) - cleanupOutput.set(false) - dryRun.set(false) + releaseNote.convention("Minor update") + inputSpecRootDirectorySkipMerge.convention(false) + modelNamePrefix.convention("") + modelNameSuffix.convention("") + apiNameSuffix.convention("") + generateModelTests.convention(true) + generateModelDocumentation.convention(true) + generateApiTests.convention(true) + generateApiDocumentation.convention(true) + configOptions.convention(mapOf()) + validateSpec.convention(true) + logToStderr.convention(false) + enablePostProcessFile.convention(false) + skipValidateSpec.convention(false) + generateAliasAsModel.convention(false) + cleanupOutput.convention(false) + dryRun.convention(false) } -} + + // ======================================================================== + // Backwards-compatibility bridge setters for Groovy/Kotlin DSL + // These allow users to continue assigning paths as standard strings. + // ======================================================================== + + /** Backwards-compatibility bridge for outputDir */ + fun setOutputDir(path: String) { + outputDir.set(project.layout.projectDirectory.dir(path)) + } + + /** Backwards-compatibility bridge for inputSpec */ + fun setInputSpec(path: String) { + if (path.isRemoteUri()) { + remoteInputSpec.set(path) + } else { + inputSpec.set(project.layout.projectDirectory.file(path)) + } + } + + /** Backwards-compatibility bridge for inputSpecRootDirectory */ + fun setInputSpecRootDirectory(path: String) { + inputSpecRootDirectory.set(project.layout.projectDirectory.dir(path)) + } + + /** Backwards-compatibility bridge for templateDir */ + fun setTemplateDir(path: String) { + templateDir.set(project.layout.projectDirectory.dir(path)) + } + + /** Backwards-compatibility bridge for configFile */ + fun setConfigFile(path: String) { + configFile.set(project.layout.projectDirectory.file(path)) + } + + /** Backwards-compatibility bridge for ignoreFileOverride */ + fun setIgnoreFileOverride(path: String) { + ignoreFileOverride.set(project.layout.projectDirectory.file(path)) + } + +} \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGeneratorsExtension.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGeneratorsExtension.kt index 8f91b1ea3420..4e5c91c69796 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGeneratorsExtension.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGeneratorsExtension.kt @@ -17,6 +17,7 @@ package org.openapitools.generator.gradle.plugin.extensions import org.gradle.api.Project +import org.gradle.api.provider.ListProperty import org.gradle.kotlin.dsl.listProperty import org.openapitools.codegen.meta.Stability @@ -29,13 +30,26 @@ open class OpenApiGeneratorGeneratorsExtension(project: Project) { /** * A list of stability indexes to include (value: all,beta,stable,experimental,deprecated). Excludes deprecated by default. */ - val include = project.objects.listProperty() + val include: ListProperty = project.objects.listProperty() init { applyDefaults() } @Suppress("MemberVisibilityCanBePrivate") - fun applyDefaults() = - include.set(Stability.values().map { it.value() }.filterNot { it == Stability.DEPRECATED.value() }) -} + fun applyDefaults() { + include.convention( + Stability.values() + .map { it.value() } + .filterNot { it == Stability.DEPRECATED.value() } + ) + } + + // ======================================================================== + // Backwards-compatibility bridge setter for Groovy/Kotlin DSL + // Allows users to continue assigning lists directly via `=` + // ======================================================================== + fun setInclude(items: Iterable) { + include.set(items) + } +} \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorMetaExtension.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorMetaExtension.kt index e78070ab120c..c664c2107b07 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorMetaExtension.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorMetaExtension.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://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, @@ -17,6 +17,8 @@ package org.openapitools.generator.gradle.plugin.extensions import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property import org.gradle.kotlin.dsl.property /** @@ -24,25 +26,37 @@ import org.gradle.kotlin.dsl.property * * @author Jim Schubert */ -open class OpenApiGeneratorMetaExtension(project: Project) { +open class OpenApiGeneratorMetaExtension(private val project: Project) { /** * The human-readable generator name of the newly created template generator. */ - val generatorName = project.objects.property() + val generatorName: Property = project.objects.property() /** * The packageName generatorName to put the main class into (defaults to org.openapitools.codegen) */ - val packageName = project.objects.property() + val packageName: Property = project.objects.property() /** * Where to write the generated files (current dir by default). */ - val outputFolder = project.objects.property() + val outputFolder: DirectoryProperty = project.objects.directoryProperty() init { - generatorName.set("default") - packageName.set("org.openapitools.codegen") - outputFolder.set("") + generatorName.convention("default") + packageName.convention("org.openapitools.codegen") + + // Use the native layout project directory instead of an empty string + outputFolder.convention(project.layout.projectDirectory) + } + + // ======================================================================== + // Backwards-compatibility bridge setter for Groovy/Kotlin DSL + // Allows users to continue assigning paths as standard strings. + // ======================================================================== + + /** Backwards-compatibility bridge for outputFolder */ + fun setOutputFolder(path: String) { + outputFolder.set(project.layout.projectDirectory.dir(path)) } } \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorValidateExtension.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorValidateExtension.kt index 3eaff1b78599..782bb643d530 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorValidateExtension.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorValidateExtension.kt @@ -17,26 +17,48 @@ package org.openapitools.generator.gradle.plugin.extensions import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property import org.gradle.kotlin.dsl.property +import org.openapitools.generator.gradle.plugin.utils.isRemoteUri /** * Gradle project level extension object definition for the generators task * * @author Jim Schubert */ -open class OpenApiGeneratorValidateExtension(project: Project) { +open class OpenApiGeneratorValidateExtension(private val project: Project) { /** * The input specification to validate. Supports all formats supported by the Parser. */ - val inputSpec = project.objects.property() + val inputSpec: RegularFileProperty = project.objects.fileProperty() + + /** + * The remote input specification to validate. Supports URLs/URIs. + */ + val remoteInputSpec: Property = project.objects.property() /** * Whether to offer recommendations related to the validated specification document. */ - val recommend = project.objects.property().convention(true) + val recommend: Property = project.objects.property().convention(true) /** * Whether to treat warnings as errors and fail the task. */ - val treatWarningsAsErrors = project.objects.property().convention(false) + val treatWarningsAsErrors: Property = project.objects.property().convention(false) + + // ======================================================================== + // Backwards-compatibility bridge setters for Groovy/Kotlin DSL + // These allow users to continue assigning paths as standard strings. + // ======================================================================== + + /** Backwards-compatibility bridge for inputSpec */ + fun setInputSpec(path: String) { + if (path.isRemoteUri()) { + remoteInputSpec.set(path) + } else { + inputSpec.set(project.layout.projectDirectory.file(path)) + } + } } \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt index 61b66a1d409a..3800fa634834 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt @@ -16,35 +16,244 @@ package org.openapitools.generator.gradle.plugin.tasks -import javax.inject.Inject +import org.gradle.api.Action import org.gradle.api.DefaultTask import org.gradle.api.GradleException -import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileSystemOperations -import org.gradle.api.model.ObjectFactory +import org.gradle.api.file.ProjectLayout +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.logging.Logging +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property -import org.gradle.api.tasks.CacheableTask -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.* import org.gradle.api.tasks.options.Option -import org.gradle.internal.logging.text.StyledTextOutput -import org.gradle.internal.logging.text.StyledTextOutputFactory -import org.gradle.kotlin.dsl.listProperty -import org.gradle.kotlin.dsl.mapProperty -import org.gradle.kotlin.dsl.property -import org.gradle.util.GradleVersion +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor import org.openapitools.codegen.CodegenConstants import org.openapitools.codegen.DefaultGenerator import org.openapitools.codegen.config.CodegenConfigurator import org.openapitools.codegen.config.GlobalSettings import org.openapitools.codegen.config.MergedSpecBuilder +import org.openapitools.generator.gradle.plugin.utils.isRemoteUri +import javax.inject.Inject + +// ========================================================================================= +// 1. WORKER API PARAMETERS +// Defines the data that safely crosses the ClassLoader boundary. +// ========================================================================================= +interface OpenApiWorkParameters : WorkParameters { + val resolvedInputSpec: Property + val outputDir: DirectoryProperty + val configFile: RegularFileProperty + val verbose: Property + val validateSpec: Property + val generatorName: Property + val auth: Property + val templateDir: DirectoryProperty + val templateResourcePath: Property + val packageName: Property + val apiPackage: Property + val modelPackage: Property + val modelNamePrefix: Property + val modelNameSuffix: Property + val apiNameSuffix: Property + val invokerPackage: Property + val groupId: Property + val id: Property + val version: Property + val library: Property + val gitHost: Property + val gitUserId: Property + val gitRepoId: Property + val releaseNote: Property + val httpUserAgent: Property + val ignoreFileOverride: RegularFileProperty + val removeOperationIdPrefix: Property + val skipOperationExample: Property + val skipOverwrite: Property + val logToStderr: Property + val enablePostProcessFile: Property + val skipValidateSpec: Property + val generateAliasAsModel: Property + val engine: Property + val dryRun: Property + + val globalProperties: MapProperty + val instantiationTypes: MapProperty + val importMappings: MapProperty + val schemaMappings: MapProperty + val inlineSchemaNameMappings: MapProperty + val inlineSchemaOptions: MapProperty + val nameMappings: MapProperty + val parameterNameMappings: MapProperty + val modelNameMappings: MapProperty + val enumNameMappings: MapProperty + val operationIdNameMappings: MapProperty + val openapiNormalizer: MapProperty + val typeMappings: MapProperty + val additionalProperties: MapProperty + val serverVariables: MapProperty + val reservedWordsMappings: MapProperty + val configOptions: MapProperty + + val languageSpecificPrimitives: ListProperty + val openapiGeneratorIgnoreList: ListProperty + + val supportingFilesConstrainedTo: ListProperty + val modelFilesConstrainedTo: ListProperty + val apiFilesConstrainedTo: ListProperty + val generateModelTests: Property + val generateModelDocumentation: Property + val generateApiTests: Property + val generateApiDocumentation: Property +} + + +// ========================================================================================= +// 2. WORKER API ACTION +// Executes the actual code generation in an isolated ClassLoader to protect GlobalSettings. +// ========================================================================================= +abstract class OpenApiWorkAction : WorkAction { + + private val logger = Logging.getLogger(OpenApiWorkAction::class.java) + + override fun execute() { + val params = parameters + + val configurator = if (params.configFile.isPresent) { + CodegenConfigurator.fromFile(params.configFile.get().asFile.absolutePath) + } else { + CodegenConfigurator() + } + + try { + // Apply Global Settings + if (params.supportingFilesConstrainedTo.orNull?.isNotEmpty() == true) { + GlobalSettings.setProperty(CodegenConstants.SUPPORTING_FILES, params.supportingFilesConstrainedTo.get().joinToString(",")) + } else { + GlobalSettings.clearProperty(CodegenConstants.SUPPORTING_FILES) + } + + if (params.modelFilesConstrainedTo.orNull?.isNotEmpty() == true) { + GlobalSettings.setProperty(CodegenConstants.MODELS, params.modelFilesConstrainedTo.get().joinToString(",")) + } else { + GlobalSettings.clearProperty(CodegenConstants.MODELS) + } + + if (params.apiFilesConstrainedTo.orNull?.isNotEmpty() == true) { + GlobalSettings.setProperty(CodegenConstants.APIS, params.apiFilesConstrainedTo.get().joinToString(",")) + } else { + GlobalSettings.clearProperty(CodegenConstants.APIS) + } + + params.generateApiDocumentation.orNull?.let { GlobalSettings.setProperty(CodegenConstants.API_DOCS, it.toString()) } + params.generateModelDocumentation.orNull?.let { GlobalSettings.setProperty(CodegenConstants.MODEL_DOCS, it.toString()) } + params.generateModelTests.orNull?.let { GlobalSettings.setProperty(CodegenConstants.MODEL_TESTS, it.toString()) } + params.generateApiTests.orNull?.let { GlobalSettings.setProperty(CodegenConstants.API_TESTS, it.toString()) } + + // Apply Configurator Settings + params.resolvedInputSpec.orNull?.let { configurator.setInputSpec(it) } + params.outputDir.orNull?.let { configurator.setOutputDir(it.asFile.absolutePath) } + params.verbose.orNull?.let { configurator.setVerbose(it) } + params.validateSpec.orNull?.let { configurator.setValidateSpec(it) } + params.skipOverwrite.orNull?.let { configurator.setSkipOverwrite(it) } + params.generatorName.orNull?.let { configurator.setGeneratorName(it) } + params.auth.orNull?.let { configurator.setAuth(it) } + + params.templateDir.orNull?.let { configurator.setTemplateDir(it.asFile.absolutePath) } + params.templateResourcePath.orNull?.let { + if (params.templateDir.isPresent) logger.warn("Both templateDir and templateResourcePath were configured. templateResourcePath overwrites templateDir.") + configurator.setTemplateDir(it) + } + + params.packageName.orNull?.let { configurator.setPackageName(it) } + params.apiPackage.orNull?.let { configurator.setApiPackage(it) } + params.modelPackage.orNull?.let { configurator.setModelPackage(it) } + params.modelNamePrefix.orNull?.let { configurator.setModelNamePrefix(it) } + params.modelNameSuffix.orNull?.let { configurator.setModelNameSuffix(it) } + params.apiNameSuffix.orNull?.let { configurator.setApiNameSuffix(it) } + params.invokerPackage.orNull?.let { configurator.setInvokerPackage(it) } + params.groupId.orNull?.let { configurator.setGroupId(it) } + params.id.orNull?.let { configurator.setArtifactId(it) } + params.version.orNull?.let { configurator.setArtifactVersion(it) } + params.library.orNull?.let { configurator.setLibrary(it) } + params.gitHost.orNull?.let { configurator.setGitHost(it) } + params.gitUserId.orNull?.let { configurator.setGitUserId(it) } + params.gitRepoId.orNull?.let { configurator.setGitRepoId(it) } + params.releaseNote.orNull?.let { configurator.setReleaseNote(it) } + params.httpUserAgent.orNull?.let { configurator.setHttpUserAgent(it) } + params.ignoreFileOverride.orNull?.let { configurator.setIgnoreFileOverride(it.asFile.absolutePath) } + params.removeOperationIdPrefix.orNull?.let { configurator.setRemoveOperationIdPrefix(it) } + params.skipOperationExample.orNull?.let { configurator.setSkipOperationExample(it) } + params.logToStderr.orNull?.let { configurator.setLogToStderr(it) } + params.enablePostProcessFile.orNull?.let { configurator.setEnablePostProcessFile(it) } + params.skipValidateSpec.orNull?.let { configurator.setValidateSpec(!it) } + params.generateAliasAsModel.orNull?.let { configurator.setGenerateAliasAsModel(it) } + + params.engine.orNull?.let { + if ("handlebars".equals(it, ignoreCase = true)) configurator.setTemplatingEngineName("handlebars") + else configurator.setTemplatingEngineName(it) + } + + // Maps and Lists + params.globalProperties.orNull?.forEach { (k, v) -> configurator.addGlobalProperty(k, v) } + params.instantiationTypes.orNull?.forEach { (k, v) -> configurator.addInstantiationType(k, v) } + params.importMappings.orNull?.forEach { (k, v) -> configurator.addImportMapping(k, v) } + params.schemaMappings.orNull?.forEach { (k, v) -> configurator.addSchemaMapping(k, v) } + params.inlineSchemaNameMappings.orNull?.forEach { (k, v) -> configurator.addInlineSchemaNameMapping(k, v) } + params.inlineSchemaOptions.orNull?.forEach { (k, v) -> configurator.addInlineSchemaOption(k, v) } + params.nameMappings.orNull?.forEach { (k, v) -> configurator.addNameMapping(k, v) } + params.parameterNameMappings.orNull?.forEach { (k, v) -> configurator.addParameterNameMapping(k, v) } + params.modelNameMappings.orNull?.forEach { (k, v) -> configurator.addModelNameMapping(k, v) } + params.enumNameMappings.orNull?.forEach { (k, v) -> configurator.addEnumNameMapping(k, v) } + params.operationIdNameMappings.orNull?.forEach { (k, v) -> configurator.addOperationIdNameMapping(k, v) } + params.openapiNormalizer.orNull?.forEach { (k, v) -> configurator.addOpenapiNormalizer(k, v) } + params.typeMappings.orNull?.forEach { (k, v) -> configurator.addTypeMapping(k, v) } + params.additionalProperties.orNull?.forEach { (k, v) -> configurator.addAdditionalProperty(k, v) } + params.serverVariables.orNull?.forEach { (k, v) -> configurator.addServerVariable(k, v) } + params.reservedWordsMappings.orNull?.forEach { (k, v) -> configurator.addAdditionalReservedWordMapping(k, v) } + + params.languageSpecificPrimitives.orNull?.forEach { configurator.addLanguageSpecificPrimitive(it) } + params.openapiGeneratorIgnoreList.orNull?.forEach { configurator.addOpenapiGeneratorIgnoreList(it) } + + val clientOptInput = configurator.toClientOptInput() + val codegenConfig = clientOptInput.config + + params.configOptions.orNull?.let { userOptions -> + codegenConfig.cliOptions().forEach { + if (userOptions.containsKey(it.opt)) { + clientOptInput.config.additionalProperties()[it.opt] = userOptions[it.opt] + } + } + } + + // Run Generator + val isDryRun = params.dryRun.getOrElse(false) + DefaultGenerator(isDryRun).opts(clientOptInput).generate() + + params.outputDir.orNull?.let { dir -> + logger.lifecycle("Successfully generated code to ${dir.asFile.absolutePath}") + } + + } catch (e: Exception) { + // Gradle's Worker API hides nested exception messages by default. + // We append the original error message to the top-level GradleException + // so it prints clearly in the console without needing --stacktrace. + val errorMessage = e.message ?: e.javaClass.simpleName + + // Optional: You can also log it explicitly to the error channel + logger.error("OpenAPI code generation failed: $errorMessage", e) + + throw GradleException("OpenAPI code generation failed: $errorMessage", e) + } finally { + // Clean up static state in this isolated ClassLoader + GlobalSettings.reset() + } + } +} /** * A task which generates the desired code. @@ -55,29 +264,43 @@ import org.openapitools.codegen.config.MergedSpecBuilder * * @author Jim Schubert */ + +// ========================================================================================= +// 3. GRADLE TASK +// Handles Gradle inputs/outputs, up-to-date checks, and submits work to the Worker API. +// ========================================================================================= @CacheableTask -open class GenerateTask @Inject constructor(private val objectFactory: ObjectFactory) : DefaultTask() { +abstract class GenerateTask : DefaultTask() { + + @get:Inject + abstract val workerExecutor: WorkerExecutor + + @get:Inject + abstract val fs: FileSystemOperations + + @get:Inject + abstract val layout: ProjectLayout /** * The verbosity of generation */ @get:Optional @get:Input - val verbose = project.objects.property() + abstract val verbose: Property /** * Whether an input specification should be validated upon generation. */ @get:Optional @get:Input - val validateSpec = project.objects.property() + abstract val validateSpec: Property /** * The name of the generator which will handle codegen. (see "openApiGenerators" task) */ @get:Optional @get:Input - val generatorName = project.objects.property() + abstract val generatorName: Property /** * This is the configuration for reference paths where schemas for openapi generation are stored @@ -86,22 +309,26 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac @get:Optional @get:InputDirectory @get:PathSensitive(PathSensitivity.ABSOLUTE) - val schemaLocation = project.objects.property() + abstract val schemaLocation: DirectoryProperty /** * The output target directory into which code will be generated. */ @get:Optional @get:OutputDirectory - val outputDir = project.objects.property() + abstract val outputDir: DirectoryProperty @Suppress("unused") - @set:Option(option = "input", description = "The input specification.") - @get:Internal - var input: String? = null - set(value) { - inputSpec.set(value) + @Option(option = "input", description = "The input specification (local path or URL/URI).") + fun setInput(value: String) { + if (value.isNotEmpty()) { + if (value.isRemoteUri()) { + remoteInputSpec.set(value) + } else { + inputSpec.set(layout.projectDirectory.file(value)) + } } + } /** * The Open API 2.0/3.x specification location. @@ -113,7 +340,7 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac @get:Optional @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) - val inputSpec = project.objects.property() + abstract val inputSpec: RegularFileProperty /** * Local root folder with spec files. @@ -124,28 +351,28 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac @get:Optional @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) - val inputSpecRootDirectory = project.objects.property(); + abstract val inputSpecRootDirectory: DirectoryProperty /** * Skip bundling all spec files into a merged spec file, if true. */ @get:Input @get:Optional - val inputSpecRootDirectorySkipMerge = project.objects.property() + abstract val inputSpecRootDirectorySkipMerge: Property /** * Name of the file that will contain all merged specs */ @get:Input @get:Optional - val mergedFileName = project.objects.property(); + abstract val mergedFileName: Property /** * The remote Open API 2.0/3.x specification URL location. */ @get:Input @get:Optional - val remoteInputSpec = project.objects.property() + abstract val remoteInputSpec: Property /** * The template directory holding a custom template. @@ -153,14 +380,14 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac @get:Optional @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) - val templateDir = project.objects.property() + abstract val templateDir: DirectoryProperty /** * Resource path containing template files. */ @get:Optional @get:Input - val templateResourcePath = project.objects.property() + abstract val templateResourcePath: Property /** * Adds authorization headers when fetching the OpenAPI definitions remotely. @@ -168,14 +395,14 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val auth = project.objects.property() + abstract val auth: Property /** * Sets specified global properties. */ @get:Optional @get:Input - val globalProperties = project.objects.mapProperty() + abstract val globalProperties: MapProperty /** * Path to json configuration file. @@ -185,70 +412,70 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac @get:Optional @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) - val configFile = project.objects.property() + abstract val configFile: RegularFileProperty /** * Specifies if the existing files should be overwritten during the generation. */ @get:Optional @get:Input - val skipOverwrite = project.objects.property() + abstract val skipOverwrite: Property /** * Package for generated classes (where supported) */ @get:Optional @get:Input - val packageName = project.objects.property() + abstract val packageName: Property /** * Package for generated api classes */ @get:Optional @get:Input - val apiPackage = project.objects.property() + abstract val apiPackage: Property /** * Package for generated models */ @get:Optional @get:Input - val modelPackage = project.objects.property() + abstract val modelPackage: Property /** * Prefix that will be prepended to all model names. Default is the empty string. */ @get:Optional @get:Input - val modelNamePrefix = project.objects.property() + abstract val modelNamePrefix: Property /** * Suffix that will be appended to all model names. Default is the empty string. */ @get:Optional @get:Input - val modelNameSuffix = project.objects.property() + abstract val modelNameSuffix: Property /** * Suffix that will be appended to all api names. Default is the empty string. */ @get:Optional @get:Input - val apiNameSuffix = project.objects.property() + abstract val apiNameSuffix: Property /** * Sets instantiation type mappings. */ @get:Optional @get:Input - val instantiationTypes = project.objects.mapProperty() + abstract val instantiationTypes: MapProperty /** * Sets mappings between OpenAPI spec types and generated code types. */ @get:Optional @get:Input - val typeMappings = project.objects.mapProperty() + abstract val typeMappings: MapProperty /** * Sets additional properties that can be referenced by the mustache templates in the format of name=value,name=value. @@ -256,7 +483,7 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val additionalProperties = project.objects.mapProperty() + abstract val additionalProperties: MapProperty /** * Sets server variable for server URL template substitution, in the format of name=value,name=value. @@ -264,168 +491,168 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val serverVariables = project.objects.mapProperty() + abstract val serverVariables: MapProperty /** * Specifies additional language specific primitive types in the format of type1,type2,type3,type3. For example: String,boolean,Boolean,Double. */ @get:Optional @get:Input - val languageSpecificPrimitives = project.objects.listProperty() + abstract val languageSpecificPrimitives: ListProperty /** * Specifies .openapi-generator-ignore list in the form of relative/path/to/file1,relative/path/to/file2. For example: README.md,pom.xml. */ @get:Optional @get:Input - val openapiGeneratorIgnoreList = project.objects.listProperty() + abstract val openapiGeneratorIgnoreList: ListProperty /** * Specifies mappings between a given class and the import that should be used for that class. */ @get:Optional @get:Input - val importMappings = project.objects.mapProperty() + abstract val importMappings: MapProperty /** * Specifies mappings between a given schema and the new one. */ @get:Optional @get:Input - val schemaMappings = project.objects.mapProperty() + abstract val schemaMappings: MapProperty /** * Specifies mappings between the inline scheme name and the new name */ @get:Optional @get:Input - val inlineSchemaNameMappings = project.objects.mapProperty() + abstract val inlineSchemaNameMappings: MapProperty /** * Specifies options for inline schemas */ @get:Optional @get:Input - val inlineSchemaOptions = project.objects.mapProperty() + abstract val inlineSchemaOptions: MapProperty /** * Specifies mappings between the property name and the new name */ @get:Optional @get:Input - val nameMappings = project.objects.mapProperty() + abstract val nameMappings: MapProperty /** * Specifies mappings between the parameter name and the new name */ @get:Optional @get:Input - val parameterNameMappings = project.objects.mapProperty() + abstract val parameterNameMappings: MapProperty /** * Specifies mappings between the model name and the new name */ @get:Optional @get:Input - val modelNameMappings = project.objects.mapProperty() + abstract val modelNameMappings: MapProperty /** * Specifies mappings between the enum name and the new name */ @get:Optional @get:Input - val enumNameMappings = project.objects.mapProperty() + abstract val enumNameMappings: MapProperty /** * Specifies mappings between the operation id name and the new name */ @get:Optional @get:Input - val operationIdNameMappings = project.objects.mapProperty() + abstract val operationIdNameMappings: MapProperty /** * Specifies mappings (rules) in OpenAPI normalizer */ @get:Optional @get:Input - val openapiNormalizer = project.objects.mapProperty() + abstract val openapiNormalizer: MapProperty /** * Root package for generated code. */ @get:Optional @get:Input - val invokerPackage = project.objects.property() + abstract val invokerPackage: Property /** * GroupId in generated pom.xml/build.gradle.kts or other build script. Language-specific conversions occur in non-jvm generators. */ @get:Optional @get:Input - val groupId = project.objects.property() + abstract val groupId: Property /** * ArtifactId in generated pom.xml/build.gradle.kts or other build script. Language-specific conversions occur in non-jvm generators. */ @get:Optional @get:Input - val id = project.objects.property() + abstract val id: Property /** * Artifact version in generated pom.xml/build.gradle.kts or other build script. Language-specific conversions occur in non-jvm generators. */ @get:Optional @get:Input - val version = project.objects.property() + abstract val version: Property /** * Reference the library template (sub-template) of a generator. */ @get:Optional @get:Input - val library = project.objects.property() + abstract val library: Property /** * Git host, e.g. gitlab.com. */ @get:Optional @get:Input - val gitHost = project.objects.property() + abstract val gitHost: Property /** * Git user ID, e.g. openapitools. */ @get:Optional @get:Input - val gitUserId = project.objects.property() + abstract val gitUserId: Property /** * Git repo ID, e.g. openapi-generator. */ @get:Optional @get:Input - val gitRepoId = project.objects.property() + abstract val gitRepoId: Property /** * Release note, default to 'Minor update'. */ @get:Optional @get:Input - val releaseNote = project.objects.property() + abstract val releaseNote: Property /** * HTTP user agent, e.g. codegen_csharp_api_client, default to 'OpenAPI-Generator/{packageVersion}/{language}' */ @get:Optional @get:Input - val httpUserAgent = project.objects.property() + abstract val httpUserAgent: Property /** * Specifies how a reserved name should be escaped to. */ @get:Optional @get:Input - val reservedWordsMappings = project.objects.mapProperty() + abstract val reservedWordsMappings: MapProperty /** * Specifies an override location for the .openapi-generator-ignore file. Most useful on initial generation. @@ -433,21 +660,21 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac @get:Optional @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) - val ignoreFileOverride = project.objects.property() + abstract val ignoreFileOverride: RegularFileProperty /** * Remove prefix of operationId, e.g. config_getId => getId */ @get:Optional @get:Input - val removeOperationIdPrefix = project.objects.property() + abstract val removeOperationIdPrefix: Property /** * Remove examples defined in the operation */ @get:Optional @get:Input - val skipOperationExample = project.objects.property() + abstract val skipOperationExample: Property /** * Defines which API-related files should be generated. This allows you to create a subset of generated files (or none at all). @@ -460,7 +687,7 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val apiFilesConstrainedTo = project.objects.listProperty() + abstract val apiFilesConstrainedTo: ListProperty /** * Defines which model-related files should be generated. This allows you to create a subset of generated files (or none at all). @@ -471,7 +698,7 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val modelFilesConstrainedTo = project.objects.listProperty() + abstract val modelFilesConstrainedTo: ListProperty /** * Defines which supporting files should be generated. This allows you to create a subset of generated files (or none at all). @@ -485,7 +712,7 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val supportingFilesConstrainedTo = project.objects.listProperty() + abstract val supportingFilesConstrainedTo: ListProperty /** * Defines whether model-related _test_ files should be generated. @@ -497,7 +724,7 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val generateModelTests = project.objects.property() + abstract val generateModelTests: Property /** * Defines whether model-related _documentation_ files should be generated. @@ -509,7 +736,7 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val generateModelDocumentation = project.objects.property() + abstract val generateModelDocumentation: Property /** * Defines whether api-related _test_ files should be generated. @@ -521,7 +748,7 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val generateApiTests = project.objects.property() + abstract val generateApiTests: Property /** * Defines whether api-related _documentation_ files should be generated. @@ -533,14 +760,14 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val generateApiDocumentation = project.objects.property() + abstract val generateApiDocumentation: Property /** * To write all log messages (not just errors) to STDOUT */ @get:Optional @get:Input - val logToStderr = project.objects.property() + abstract val logToStderr: Property /** * To enable the file post-processing hook. This enables executing an external post-processor (usually a linter program). @@ -550,14 +777,14 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val enablePostProcessFile = project.objects.property() + abstract val enablePostProcessFile: Property /** * To skip spec validation. When true, we will skip the default behavior of validating a spec before generation. */ @get:Optional @get:Input - val skipValidateSpec = project.objects.property() + abstract val skipValidateSpec: Property /** * To generate alias (array, list, map) as model. When false, top-level objects defined as array, list, or map will result in those @@ -566,21 +793,21 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val generateAliasAsModel = project.objects.property() + abstract val generateAliasAsModel: Property /** * A dynamic map of options specific to a generator. */ @get:Optional @get:Input - val configOptions = project.objects.mapProperty() + abstract val configOptions: MapProperty /** * Templating engine: "mustache" (default) or "handlebars" (beta) */ @get:Optional @get:Input - val engine = project.objects.property() + abstract val engine: Property /** * Defines whether the output dir should be cleaned up before generating the output. @@ -588,417 +815,122 @@ open class GenerateTask @Inject constructor(private val objectFactory: ObjectFac */ @get:Optional @get:Input - val cleanupOutput = project.objects.property() + abstract val cleanupOutput: Property /** * Defines whether the generator should run in dry-run mode. */ @get:Optional @get:Input - val dryRun = project.objects.property() - - private fun Property.ifNotEmpty(block: Property.(T) -> Unit) { - if (isPresent) { - when (val value = get()) { - is String -> if (value.isNotEmpty()) block(value) - else -> block(value) - } - } - } - - protected open fun createDefaultCodegenConfigurator(): CodegenConfigurator = CodegenConfigurator() + abstract val dryRun: Property - private fun createFileSystemManager(): FileSystemManager { - return if(GradleVersion.current() >= GradleVersion.version("6.0")) { - objectFactory.newInstance(FileSystemManagerDefault::class.java) - } else { - objectFactory.newInstance(FileSystemManagerLegacy::class.java, project) - } + init { + inputSpecRootDirectorySkipMerge.convention(false) + mergedFileName.convention("merged") } @Suppress("unused") @TaskAction fun doWork() { - var resolvedInputSpec = "" + var finalResolvedInputSpec = "" - inputSpec.ifNotEmpty { value -> - resolvedInputSpec = value + if (inputSpec.isPresent && remoteInputSpec.isPresent) { + logger.warn("Both inputSpec and remoteInputSpec are specified. The remoteInputSpec takes priority.") } - remoteInputSpec.ifNotEmpty { value -> - resolvedInputSpec = value - } + inputSpec.orNull?.let { finalResolvedInputSpec = it.asFile.absolutePath } - inputSpecRootDirectory.ifNotEmpty { inputSpecRootDirectoryValue -> - val skipMerge = inputSpecRootDirectorySkipMerge.get() - val runMergeSpec = !skipMerge - if (runMergeSpec) { - run { - resolvedInputSpec = MergedSpecBuilder( - inputSpecRootDirectoryValue, - mergedFileName.getOrElse("merged") - ).buildMergedSpec() - logger.info("Merge input spec would be used - {}", resolvedInputSpec) - } - } + remoteInputSpec.orNull?.takeIf { it.isNotEmpty() }?.let { + finalResolvedInputSpec = it + logger.warn("Using remoteInputSpec may result in stale build caches if the remote content changes.") } - cleanupOutput.ifNotEmpty { cleanup -> - if (cleanup) { - createFileSystemManager().delete(outputDir) - val out = services.get(StyledTextOutputFactory::class.java).create("openapi") - out.withStyle(StyledTextOutput.Style.Success) - out.println("Cleaned up output directory ${outputDir.get()} before code generation (cleanupOutput set to true).") + inputSpecRootDirectory.orNull?.let { inputDir -> + if (!inputSpecRootDirectorySkipMerge.get()) { + finalResolvedInputSpec = MergedSpecBuilder( + inputDir.asFile.absolutePath, + mergedFileName.get() + ).buildMergedSpec() + logger.info("Merge input spec used: {}", finalResolvedInputSpec) } } - val configurator: CodegenConfigurator = if (configFile.isPresent) { - CodegenConfigurator.fromFile(configFile.get()) - } else createDefaultCodegenConfigurator() - - try { - if (globalProperties.isPresent) { - globalProperties.get().forEach { (key, value) -> - configurator.addGlobalProperty(key, value) - } - } - - if (supportingFilesConstrainedTo.isPresent && supportingFilesConstrainedTo.get().isNotEmpty()) { - GlobalSettings.setProperty( - CodegenConstants.SUPPORTING_FILES, - supportingFilesConstrainedTo.get().joinToString(",") - ) - } else { - GlobalSettings.clearProperty(CodegenConstants.SUPPORTING_FILES) - } - - if (modelFilesConstrainedTo.isPresent && modelFilesConstrainedTo.get().isNotEmpty()) { - GlobalSettings.setProperty(CodegenConstants.MODELS, modelFilesConstrainedTo.get().joinToString(",")) - } else { - GlobalSettings.clearProperty(CodegenConstants.MODELS) - } - - if (apiFilesConstrainedTo.isPresent && apiFilesConstrainedTo.get().isNotEmpty()) { - GlobalSettings.setProperty(CodegenConstants.APIS, apiFilesConstrainedTo.get().joinToString(",")) - } else { - GlobalSettings.clearProperty(CodegenConstants.APIS) - } - - if (generateApiDocumentation.isPresent) { - GlobalSettings.setProperty(CodegenConstants.API_DOCS, generateApiDocumentation.get().toString()) - } - - if (generateModelDocumentation.isPresent) { - GlobalSettings.setProperty(CodegenConstants.MODEL_DOCS, generateModelDocumentation.get().toString()) - } - - if (generateModelTests.isPresent) { - GlobalSettings.setProperty(CodegenConstants.MODEL_TESTS, generateModelTests.get().toString()) - } - - if (generateApiTests.isPresent) { - GlobalSettings.setProperty(CodegenConstants.API_TESTS, generateApiTests.get().toString()) - } - - if (inputSpec.isPresent && remoteInputSpec.isPresent) { - logger.warn("Both inputSpec and remoteInputSpec is specified. The remoteInputSpec will take priority over inputSpec.") - } - - configurator.setInputSpec(resolvedInputSpec) - - // now override with any specified parameters - verbose.ifNotEmpty { value -> - configurator.setVerbose(value) - } - - validateSpec.ifNotEmpty { value -> - configurator.setValidateSpec(value) - } - - skipOverwrite.ifNotEmpty { value -> - configurator.setSkipOverwrite(value) - } - - generatorName.ifNotEmpty { value -> - configurator.setGeneratorName(value) - } - - outputDir.ifNotEmpty { value -> - configurator.setOutputDir(value) - } - - auth.ifNotEmpty { value -> - configurator.setAuth(value) - } - - templateDir.ifNotEmpty { value -> - configurator.setTemplateDir(value) - } - - templateResourcePath.ifNotEmpty { value -> - templateDir.ifNotEmpty { - logger.warn("Both templateDir and templateResourcePath were configured. templateResourcePath overwrites templateDir.") - } - configurator.setTemplateDir(value) - } - - packageName.ifNotEmpty { value -> - configurator.setPackageName(value) - } - - apiPackage.ifNotEmpty { value -> - configurator.setApiPackage(value) - } - - modelPackage.ifNotEmpty { value -> - configurator.setModelPackage(value) - } - - modelNamePrefix.ifNotEmpty { value -> - configurator.setModelNamePrefix(value) - } - - modelNameSuffix.ifNotEmpty { value -> - configurator.setModelNameSuffix(value) - } - - apiNameSuffix.ifNotEmpty { value -> - configurator.setApiNameSuffix(value) - } - - invokerPackage.ifNotEmpty { value -> - configurator.setInvokerPackage(value) + cleanupOutput.orNull?.let { cleanup -> + if (cleanup && outputDir.isPresent) { + fs.delete { delete(outputDir) } + logger.lifecycle("Cleaned up output directory ${outputDir.get().asFile.path} before code generation.") } - - groupId.ifNotEmpty { value -> - configurator.setGroupId(value) - } - - id.ifNotEmpty { value -> - configurator.setArtifactId(value) - } - - version.ifNotEmpty { value -> - configurator.setArtifactVersion(value) - } - - library.ifNotEmpty { value -> - configurator.setLibrary(value) - } - - gitHost.ifNotEmpty { value -> - configurator.setGitHost(value) - } - - gitUserId.ifNotEmpty { value -> - configurator.setGitUserId(value) - } - - gitRepoId.ifNotEmpty { value -> - configurator.setGitRepoId(value) - } - - releaseNote.ifNotEmpty { value -> - configurator.setReleaseNote(value) - } - - httpUserAgent.ifNotEmpty { value -> - configurator.setHttpUserAgent(value) - } - - ignoreFileOverride.ifNotEmpty { value -> - configurator.setIgnoreFileOverride(value) - } - - removeOperationIdPrefix.ifNotEmpty { value -> - configurator.setRemoveOperationIdPrefix(value) - } - - skipOperationExample.ifNotEmpty { value -> - configurator.setSkipOperationExample(value) - } - - logToStderr.ifNotEmpty { value -> - configurator.setLogToStderr(value) - } - - enablePostProcessFile.ifNotEmpty { value -> - configurator.setEnablePostProcessFile(value) - } - - skipValidateSpec.ifNotEmpty { value -> - configurator.setValidateSpec(!value) - } - - generateAliasAsModel.ifNotEmpty { value -> - configurator.setGenerateAliasAsModel(value) - } - - engine.ifNotEmpty { value -> - if ("handlebars".equals(value, ignoreCase = true)) { - configurator.setTemplatingEngineName("handlebars") - } else { - configurator.setTemplatingEngineName(value) - } - } - - if (globalProperties.isPresent) { - globalProperties.get().forEach { entry -> - configurator.addGlobalProperty(entry.key, entry.value) - } - } - - if (instantiationTypes.isPresent) { - instantiationTypes.get().forEach { entry -> - configurator.addInstantiationType(entry.key, entry.value) - } - } - - if (importMappings.isPresent) { - importMappings.get().forEach { entry -> - configurator.addImportMapping(entry.key, entry.value) - } - } - - if (schemaMappings.isPresent) { - schemaMappings.get().forEach { entry -> - configurator.addSchemaMapping(entry.key, entry.value) - } - } - - if (inlineSchemaNameMappings.isPresent) { - inlineSchemaNameMappings.get().forEach { entry -> - configurator.addInlineSchemaNameMapping(entry.key, entry.value) - } - } - - if (inlineSchemaOptions.isPresent) { - inlineSchemaOptions.get().forEach { entry -> - configurator.addInlineSchemaOption(entry.key, entry.value) - } - } - - if (nameMappings.isPresent) { - nameMappings.get().forEach { entry -> - configurator.addNameMapping(entry.key, entry.value) - } - } - - if (parameterNameMappings.isPresent) { - parameterNameMappings.get().forEach { entry -> - configurator.addParameterNameMapping(entry.key, entry.value) - } - } - - if (modelNameMappings.isPresent) { - modelNameMappings.get().forEach { entry -> - configurator.addModelNameMapping(entry.key, entry.value) - } - } - - if (enumNameMappings.isPresent) { - enumNameMappings.get().forEach { entry -> - configurator.addEnumNameMapping(entry.key, entry.value) - } - } - - if (operationIdNameMappings.isPresent) { - operationIdNameMappings.get().forEach { entry -> - configurator.addOperationIdNameMapping(entry.key, entry.value) - } - } - - if (openapiNormalizer.isPresent) { - openapiNormalizer.get().forEach { entry -> - configurator.addOpenapiNormalizer(entry.key, entry.value) - } - } - - if (typeMappings.isPresent) { - typeMappings.get().forEach { entry -> - configurator.addTypeMapping(entry.key, entry.value) - } - } - - if (additionalProperties.isPresent) { - additionalProperties.get().forEach { entry -> - configurator.addAdditionalProperty(entry.key, entry.value) - } - } - - if (serverVariables.isPresent) { - serverVariables.get().forEach { entry -> - configurator.addServerVariable(entry.key, entry.value) - } - } - - if (languageSpecificPrimitives.isPresent) { - languageSpecificPrimitives.get().forEach { - configurator.addLanguageSpecificPrimitive(it) - } - } - - if (openapiGeneratorIgnoreList.isPresent) { - openapiGeneratorIgnoreList.get().forEach { - configurator.addOpenapiGeneratorIgnoreList(it) - } - } - - if (reservedWordsMappings.isPresent) { - reservedWordsMappings.get().forEach { entry -> - configurator.addAdditionalReservedWordMapping(entry.key, entry.value) - } - } - - var dryRunSetting = false - dryRun.ifNotEmpty { setting -> - dryRunSetting = setting - } - - val clientOptInput = configurator.toClientOptInput() - val codegenConfig = clientOptInput.config - - if (configOptions.isPresent) { - val userSpecifiedConfigOptions = configOptions.get() - codegenConfig.cliOptions().forEach { - if (userSpecifiedConfigOptions.containsKey(it.opt)) { - clientOptInput.config.additionalProperties()[it.opt] = userSpecifiedConfigOptions[it.opt] - } - } - } - - try { - val out = services.get(StyledTextOutputFactory::class.java).create("openapi") - out.withStyle(StyledTextOutput.Style.Success) - - DefaultGenerator(dryRunSetting).opts(clientOptInput).generate() - - out.println("Successfully generated code to ${outputDir.get()}") - } catch (e: RuntimeException) { - throw GradleException("Code generation failed.", e) - } - } finally { - GlobalSettings.reset() } - } -} - -internal interface FileSystemManager { - - fun delete(outputDir: Property) - -} - -internal open class FileSystemManagerLegacy @Inject constructor(private val project: Project): FileSystemManager { - override fun delete(outputDir: Property) { - project.delete(outputDir) +// Submit generation logic to the isolated Worker API Queue + val workQueue = workerExecutor.classLoaderIsolation() + + workQueue.submit(OpenApiWorkAction::class.java, object : Action { + override fun execute(parameters: OpenApiWorkParameters) { + parameters.resolvedInputSpec.set(finalResolvedInputSpec) + parameters.outputDir.set(outputDir) + parameters.configFile.set(configFile) + parameters.verbose.set(verbose) + parameters.validateSpec.set(validateSpec) + parameters.generatorName.set(generatorName) + parameters.auth.set(auth) + parameters.templateDir.set(templateDir) + parameters.templateResourcePath.set(templateResourcePath) + parameters.packageName.set(packageName) + parameters.apiPackage.set(apiPackage) + parameters.modelPackage.set(modelPackage) + parameters.modelNamePrefix.set(modelNamePrefix) + parameters.modelNameSuffix.set(modelNameSuffix) + parameters.apiNameSuffix.set(apiNameSuffix) + parameters.invokerPackage.set(invokerPackage) + parameters.groupId.set(groupId) + parameters.id.set(id) + parameters.version.set(version) + parameters.library.set(library) + parameters.gitHost.set(gitHost) + parameters.gitUserId.set(gitUserId) + parameters.gitRepoId.set(gitRepoId) + parameters.releaseNote.set(releaseNote) + parameters.httpUserAgent.set(httpUserAgent) + parameters.ignoreFileOverride.set(ignoreFileOverride) + parameters.removeOperationIdPrefix.set(removeOperationIdPrefix) + parameters.skipOperationExample.set(skipOperationExample) + parameters.skipOverwrite.set(skipOverwrite) + parameters.logToStderr.set(logToStderr) + parameters.enablePostProcessFile.set(enablePostProcessFile) + parameters.skipValidateSpec.set(skipValidateSpec) + parameters.generateAliasAsModel.set(generateAliasAsModel) + parameters.engine.set(engine) + parameters.dryRun.set(dryRun) + + parameters.globalProperties.set(globalProperties) + parameters.instantiationTypes.set(instantiationTypes) + parameters.importMappings.set(importMappings) + parameters.schemaMappings.set(schemaMappings) + parameters.inlineSchemaNameMappings.set(inlineSchemaNameMappings) + parameters.inlineSchemaOptions.set(inlineSchemaOptions) + parameters.nameMappings.set(nameMappings) + parameters.parameterNameMappings.set(parameterNameMappings) + parameters.modelNameMappings.set(modelNameMappings) + parameters.enumNameMappings.set(enumNameMappings) + parameters.operationIdNameMappings.set(operationIdNameMappings) + parameters.openapiNormalizer.set(openapiNormalizer) + parameters.typeMappings.set(typeMappings) + parameters.additionalProperties.set(additionalProperties) + parameters.serverVariables.set(serverVariables) + parameters.reservedWordsMappings.set(reservedWordsMappings) + parameters.configOptions.set(configOptions) + + parameters.languageSpecificPrimitives.set(languageSpecificPrimitives) + parameters.openapiGeneratorIgnoreList.set(openapiGeneratorIgnoreList) + parameters.supportingFilesConstrainedTo.set(supportingFilesConstrainedTo) + parameters.modelFilesConstrainedTo.set(modelFilesConstrainedTo) + parameters.apiFilesConstrainedTo.set(apiFilesConstrainedTo) + parameters.generateModelTests.set(generateModelTests) + parameters.generateModelDocumentation.set(generateModelDocumentation) + parameters.generateApiTests.set(generateApiTests) + parameters.generateApiDocumentation.set(generateApiDocumentation) + } + }) } -} - -internal open class FileSystemManagerDefault @Inject constructor(private val fs: FileSystemOperations) : FileSystemManager { - - override fun delete(outputDir: Property) { - fs.delete { delete(outputDir) } - } -} +} \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GeneratorsTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GeneratorsTask.kt index d83d00b5b637..2a2c255fb500 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GeneratorsTask.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GeneratorsTask.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://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, @@ -17,11 +17,9 @@ package org.openapitools.generator.gradle.plugin.tasks import org.gradle.api.DefaultTask +import org.gradle.api.provider.ListProperty import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction -import org.gradle.internal.logging.text.StyledTextOutput -import org.gradle.internal.logging.text.StyledTextOutputFactory -import org.gradle.kotlin.dsl.listProperty import org.gradle.work.DisableCachingByDefault import org.openapitools.codegen.CodegenConfigLoader import org.openapitools.codegen.CodegenType @@ -38,66 +36,57 @@ import org.openapitools.codegen.meta.Stability * @author Jim Schubert */ @DisableCachingByDefault(because = "not worth caching") -open class GeneratorsTask : DefaultTask() { +abstract class GeneratorsTask : DefaultTask() { + /** * A list of stability indexes to include (value: all,beta,stable,experimental,deprecated). Excludes deprecated by default. */ @get:Internal - val include = project.objects.listProperty() + abstract val include: ListProperty @TaskAction fun doWork() { val generators = CodegenConfigLoader.getAll() - val out = services.get(StyledTextOutputFactory::class.java).create("openapi") - - StringBuilder().apply { - val types = CodegenType.values() - - val stabilities = if (include.isPresent) { - when { - include.get().contains("all") -> Stability.values().toList() - else -> include.get().map { Stability.forDescription(it) } - } + // Safely extract the includes, falling back to the default if empty or not present + val stabilities = include.orNull?.takeIf { it.isNotEmpty() }?.let { includes -> + if (includes.contains("all")) { + Stability.values().toList() } else { - Stability.values().filterNot { it == Stability.DEPRECATED } + includes.map { Stability.forDescription(it) } } + } ?: Stability.values().filterNot { it == Stability.DEPRECATED } - append("The following generators are available:") - - append(System.lineSeparator()) - append(System.lineSeparator()) + val sb = StringBuilder() + sb.append("The following generators are available:\n\n") - for (type in types) { - append(type.name).append(" generators:") - append(System.lineSeparator()) + for (type in CodegenType.values()) { + sb.append(type.name).append(" generators:\n") - generators.filter { it.tag == type } - .sortedBy { it.name } - .forEach { generator -> + generators.filter { it.tag == type } + .sortedBy { it.name } + .forEach { generator -> + val meta: GeneratorMetadata? = generator.generatorMetadata + val shouldInclude = stabilities.contains(meta?.stability) - val meta: GeneratorMetadata? = generator.generatorMetadata - val include = stabilities.contains(meta?.stability) - if (include) { - append(" - ") - append(generator.name) + if (shouldInclude) { + sb.append(" - ") + sb.append(generator.name) - meta?.stability?.let { - if (it != Stability.STABLE) { - append(" (${it.value()})") - } - } - - append(System.lineSeparator()) + meta?.stability?.let { stability -> + if (stability != Stability.STABLE) { + sb.append(" (${stability.value()})") } } - append(System.lineSeparator()) - append(System.lineSeparator()) - } + sb.append("\n") + } + } - out.withStyle(StyledTextOutput.Style.Success) - out.formatln("%s%n", toString()) + sb.append("\n\n") } + + // Use Gradle's standard lifecycle logger instead of internal StyledTextOutputFactory + logger.lifecycle(sb.toString()) } } \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/MetaTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/MetaTask.kt index 8449cd75edbc..c9c9dda8d42f 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/MetaTask.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/MetaTask.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://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, @@ -19,13 +19,12 @@ package org.openapitools.generator.gradle.plugin.tasks import com.samskivert.mustache.Mustache import org.gradle.api.DefaultTask import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction -import org.gradle.internal.logging.text.StyledTextOutput -import org.gradle.internal.logging.text.StyledTextOutputFactory -import org.gradle.kotlin.dsl.property import org.openapitools.codegen.CodegenConfig import org.openapitools.codegen.CodegenConstants import org.openapitools.codegen.SupportingFile @@ -43,28 +42,25 @@ import java.nio.charset.Charset * @author Jim Schubert */ @CacheableTask -open class MetaTask : DefaultTask() { +abstract class MetaTask : DefaultTask() { + @get:Input - val generatorName = project.objects.property() + abstract val generatorName: Property @get:Input - val packageName = project.objects.property() + abstract val packageName: Property @get:OutputDirectory - val outputFolder = project.objects.property() + abstract val outputFolder: DirectoryProperty @TaskAction fun doWork() { val packageToPath = packageName.get().replace(".", File.separator) - val dir = File(outputFolder.get()) + val dir = outputFolder.get().asFile val klass = "${generatorName.get().titleCasedTextOnly()}Generator" val templateResourceDir = generatorName.get().hyphenatedTextOnly() - val out = services.get(StyledTextOutputFactory::class.java).create("openapi") - - out.withStyle(StyledTextOutput.Style.Info) - logger.debug("package: {}", packageName.get()) logger.debug("dir: {}", dir.absolutePath) logger.debug("generator class: {}", klass) @@ -83,7 +79,7 @@ open class MetaTask : DefaultTask() { ) ) - val currentVersion = CodegenConstants::class.java.`package`.implementationVersion + val currentVersion = CodegenConstants::class.java.`package`.implementationVersion ?: "unknown" val data = mapOf( "generatorPackage" to packageToPath, @@ -93,9 +89,9 @@ open class MetaTask : DefaultTask() { "openapiGeneratorVersion" to currentVersion ) - supportingFiles.map { + supportingFiles.forEach { try { - val destinationFolder = File(File(dir.absolutePath), it.folder) + val destinationFolder = File(dir, it.folder) destinationFolder.mkdirs() val outputFile = File(destinationFolder, it.destinationFilename) @@ -121,24 +117,22 @@ open class MetaTask : DefaultTask() { outputFile.writeText(formatted, Charset.forName("UTF8")) - out.formatln("Wrote file to %s", outputFile.absolutePath) + logger.lifecycle("Wrote file to ${outputFile.absolutePath}") - // TODO: register outputs - // return outputFile } catch (e: IOException) { - logger.error(e.message) + logger.error("Failed to generate file: ${e.message}", e) throw GradleException("Can't generate project", e) } } - out.withStyle(StyledTextOutput.Style.Success) - out.formatln("Created generator %s", klass) + + logger.lifecycle("Created generator $klass") } private fun String.titleCasedTextOnly(): String = - split(Regex("[^a-zA-Z0-9]")).joinToString(separator = "", transform = String::capitalize) + split(Regex("[^a-zA-Z0-9]")).joinToString(separator = "") { it.capitalize() } private fun String.hyphenatedTextOnly(): String = - split(Regex("[^a-zA-Z0-9]")).joinToString(separator = "-", transform = String::toLowerCase) + split(Regex("[^a-zA-Z0-9]")).joinToString(separator = "-") { it.toLowerCase() } private fun dir(vararg parts: String): String = parts.joinToString(separator = File.separator) -} +} \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/ValidateTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/ValidateTask.kt index 4d2d13836541..239b97960319 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/ValidateTask.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/ValidateTask.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://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, @@ -20,21 +20,15 @@ import io.swagger.parser.OpenAPIParser import io.swagger.v3.parser.core.models.ParseOptions import org.gradle.api.DefaultTask import org.gradle.api.GradleException -import org.gradle.api.logging.Logging -import org.gradle.api.tasks.CacheableTask -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction +import org.gradle.api.file.ProjectLayout +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* import org.gradle.api.tasks.options.Option -import org.gradle.internal.logging.text.StyledTextOutput -import org.gradle.internal.logging.text.StyledTextOutputFactory -import org.gradle.kotlin.dsl.property import org.openapitools.codegen.validations.oas.OpenApiEvaluator import org.openapitools.codegen.validations.oas.RuleConfiguration +import org.openapitools.generator.gradle.plugin.utils.isRemoteUri +import javax.inject.Inject /** * A generator which validates an Open API spec. This task outputs a list of validation issues and errors. @@ -42,53 +36,76 @@ import org.openapitools.codegen.validations.oas.RuleConfiguration * Example: * cli: * - * ./gradlew openApiValidate --input=/path/to/file + * ./gradlew openApiValidate --input=/path/to/file * * build.gradle.kts: * - * openApiMeta { - * inputSpec = "path/to/spec.yaml" - * } + * openApiValidate { + * inputSpec.set(layout.projectDirectory.file("path/to/spec.yaml")) + * } * * @author Jim Schubert */ @CacheableTask -open class ValidateTask : DefaultTask() { +abstract class ValidateTask : DefaultTask() { + + @get:Inject + abstract val layout: ProjectLayout + + @get:Optional @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) - val inputSpec = project.objects.property() + abstract val inputSpec: RegularFileProperty + + @get:Optional + @get:Input + abstract val remoteInputSpec: Property @get:Optional @get:Input - val recommend = project.objects.property().convention(true) + abstract val recommend: Property @get:Optional @get:Input - val treatWarningsAsErrors = project.objects.property().convention(false) + abstract val treatWarningsAsErrors: Property - @get:Internal - @set:Option(option = "input", description = "The input specification.") - var input: String? = null - set(value) { - inputSpec.set(value) + init { + recommend.convention(true) + treatWarningsAsErrors.convention(false) + } + + @Suppress("unused") + @Option(option = "input", description = "The input specification (local path or URL).") + fun setInput(value: String) { + if (value.isNotEmpty()) { + if (value.isRemoteUri()) { + remoteInputSpec.set(value) + } else { + inputSpec.set(layout.projectDirectory.file(value)) + } } + } @TaskAction fun doWork() { - val logger = Logging.getLogger(javaClass) + // Evaluate inputs - prefer remote if provided, fallback to local file + val specLocation = remoteInputSpec.orNull ?: inputSpec.orNull?.asFile?.absolutePath + + if (specLocation == null) { + throw GradleException("You must configure either inputSpec or provide a valid remote input via --input") + } - val spec = inputSpec.get() val recommendations = recommend.get() val failOnWarnings = treatWarningsAsErrors.get() - logger.quiet("Validating spec $spec") + logger.lifecycle("Validating spec $specLocation") val options = ParseOptions() options.isResolve = true - val result = OpenAPIParser().readLocation(spec, null, options) + // Pass specLocation instead of specPath + val result = OpenAPIParser().readLocation(specLocation, null, options) val messages = result.messages.toSet() - val out = services.get(StyledTextOutputFactory::class.java).create("openapi") val ruleConfiguration = RuleConfiguration() ruleConfiguration.isEnableRecommendations = recommendations @@ -96,44 +113,41 @@ open class ValidateTask : DefaultTask() { val evaluator = OpenApiEvaluator(ruleConfiguration) val validationResult = evaluator.validate(result.openAPI) - if (validationResult.warnings.isNotEmpty()) { - out.withStyle(StyledTextOutput.Style.Info) - out.println("\nSpec has issues or recommendations.\nIssues:\n") + val hasErrors = messages.isNotEmpty() || validationResult.errors.isNotEmpty() + val hasWarnings = validationResult.warnings.isNotEmpty() + if (hasWarnings) { + logger.warn("\nSpec has issues or recommendations.\nIssues:\n") validationResult.warnings.forEach { - out.withStyle(StyledTextOutput.Style.Info) - out.println("\t${it.message}\n") + logger.warn("\t${it.message}") logger.debug("WARNING: ${it.message}|${it.details}") } + logger.warn("") // spacing line } - if (messages.isNotEmpty() || validationResult.errors.isNotEmpty()) { - out.withStyle(StyledTextOutput.Style.Error) - out.println("\nSpec is invalid.\nIssues:\n") + if (hasErrors) { + logger.error("\nSpec is invalid.\nIssues:\n") messages.forEach { - out.withStyle(StyledTextOutput.Style.Error) - out.println("\t$it\n") + logger.error("\t$it") logger.debug("ERROR: $it") } validationResult.errors.forEach { - out.withStyle(StyledTextOutput.Style.Error) - out.println("\t${it.message}\n") + logger.error("\t${it.message}") logger.debug("ERROR: ${it.message}|${it.details}") } + logger.error("") // spacing line - throw GradleException("Validation failed.") + throw GradleException("Validation failed. Spec is invalid.") } - if (failOnWarnings && validationResult.warnings.isNotEmpty()) { - out.withStyle(StyledTextOutput.Style.Error) - out.println("\nWarnings found in the spec and 'treatWarningsAsErrors' is enabled.\nFailing validation.\n") + if (failOnWarnings && hasWarnings) { + logger.error("\nWarnings found in the spec and 'treatWarningsAsErrors' is enabled.\nFailing validation.\n") throw GradleException("Validation failed due to warnings (treatWarningsAsErrors = true).") } - out.withStyle(StyledTextOutput.Style.Success) logger.debug("No error validations from swagger-parser or internal validations.") - out.println("Spec is valid.") + logger.lifecycle("Spec is valid.") } -} +} \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/utils/StringUtils.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/utils/StringUtils.kt new file mode 100644 index 000000000000..bb75655eda9c --- /dev/null +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/utils/StringUtils.kt @@ -0,0 +1,11 @@ +package org.openapitools.generator.gradle.plugin.utils + +private val remoteUriRegex = "^[a-zA-Z][a-zA-Z0-9+\\-.]+?:.*".toRegex() + +/** + * Determines if a string is a remote URI (e.g., http:, jar:, s3:) + * while safely ignoring 1-letter Windows drives (e.g., C:\). + */ +internal fun String.isRemoteUri(): Boolean { + return this.matches(remoteUriRegex) +} diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskConfigurationCacheTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskConfigurationCacheTest.kt index 7f5c8fea1cf8..1bcaac70a86a 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskConfigurationCacheTest.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskConfigurationCacheTest.kt @@ -1,7 +1,6 @@ package org.openapitools.generator.gradle.plugin import org.gradle.testkit.runner.TaskOutcome -import org.testng.SkipException import org.testng.annotations.BeforeMethod import org.testng.annotations.DataProvider import org.testng.annotations.Test @@ -20,23 +19,26 @@ class GenerateTaskConfigurationCacheTest : TestBase() { } @DataProvider(name = "gradle_version_provider") - private fun gradleVersionProviderWithConfigurationCache(): Array> = arrayOf(arrayOf("8.14.4"), arrayOf("8.5")) - - @DataProvider(name = "gradle_version_provider_without_cc") - private fun gradleVersionProviderWithoutConfigurationCache(): Array> = arrayOf(arrayOf("5.6.1")) + private fun gradleVersionProviderWithConfigurationCache(): Array> = arrayOf( + arrayOf("8.14.4", "STRING"), + arrayOf("8.14.4", "FILE"), + arrayOf("8.5", "STRING"), + arrayOf("8.5", "FILE"), + ) // inputSpec tests - private val inputSpecExtensionContents = """ + private fun inputSpecExtensionContents(format: PropertyFormat) = """ generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} cleanupOutput.set(true) """.trimIndent() @Test(dataProvider = "gradle_version_provider") - fun `openApiGenerate should reuse configuration cache`(gradleVersion: String) { + fun `openApiGenerate should reuse configuration cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange - withProject(inputSpecExtensionContents) + withProject(inputSpecExtensionContents(propertyFormat)) // Act val result1 = build { @@ -61,41 +63,54 @@ class GenerateTaskConfigurationCacheTest : TestBase() { assertEquals(expectedRelativeFilePathSet, projectDirCC.toRelativeFilePathSet()) } - private fun getJavaVersion(): Int { - val version = System.getProperty("java.version") - val parts = version.split('.') - if (parts.first() == "1") return parts.getOrElse(1) { "0" }.toInt() - return parts.first().toInt() - } - - @Test(dataProvider = "gradle_version_provider_without_cc") - fun `openApiGenerate should work with Gradle legacy versions`(gradleVersion: String) { - if(getJavaVersion() > 12) { - // https://docs.gradle.org/current/userguide/compatibility.html - throw SkipException("Skipping test as Gradle ${gradleVersion} is not compatible with Java ${getJavaVersion()}") - } + @Test(dataProvider = "gradle_version_provider") + fun `openApiGenerate should handle up-to-date with configuration cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange - withProject(inputSpecExtensionContents) + withProject(inputSpecExtensionContents(propertyFormat)) - // Act + // Act - First run: Should be SUCCESS and store the configuration cache val result1 = build { withProjectDir(projectDirCC) - withArguments("clean", "openApiGenerate") + withArguments("--configuration-cache", "openApiGenerate") withGradleVersion(gradleVersion) } - val expectedRelativeFilePathSet = projectDirCC.toRelativeFilePathSet() + // Assert first run + assertEquals(TaskOutcome.SUCCESS, result1.task(":openApiGenerate")?.outcome) + assertTrue(result1.output.contains("Configuration cache entry stored.")) + // Act - Second run: Should be UP-TO-DATE and reuse the cache val result2 = build { withProjectDir(projectDirCC) - withArguments("clean", "openApiGenerate") + withArguments("--configuration-cache", "openApiGenerate") withGradleVersion(gradleVersion) } - // Assert - assertEquals(TaskOutcome.SUCCESS, result1.task(":openApiGenerate")?.outcome) - assertEquals(TaskOutcome.SUCCESS, result2.task(":openApiGenerate")?.outcome) - assertEquals(expectedRelativeFilePathSet, projectDirCC.toRelativeFilePathSet()) + // Assert second run + assertEquals(TaskOutcome.UP_TO_DATE, result2.task(":openApiGenerate")?.outcome) + assertTrue(result2.output.contains("Configuration cache entry reused.")) + + // Act - Third run: Modify spec file and task should re-execute + val specFile = projectDirCC.resolve("spec.yaml") + specFile.appendText("\n# Trigger change") + + val result3 = build { + withProjectDir(projectDirCC) + withArguments("--configuration-cache", "openApiGenerate") + withGradleVersion(gradleVersion) + } + + // Assert third run + assertEquals(TaskOutcome.SUCCESS, result3.task(":openApiGenerate")?.outcome) + assertTrue(result3.output.contains("Configuration cache entry reused.")) + } + + private fun getJavaVersion(): Int { + val version = System.getProperty("java.version") + val parts = version.split('.') + if (parts.first() == "1") return parts.getOrElse(1) { "0" }.toInt() + return parts.first().toInt() } // Helper methods & test fixtures diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskDslTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskDslTest.kt index cf7e688c359b..08e350aa86c5 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskDslTest.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskDslTest.kt @@ -2,6 +2,7 @@ package org.openapitools.generator.gradle.plugin import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome +import org.testng.annotations.DataProvider import org.testng.annotations.Test import java.io.File import java.nio.file.Files.createDirectory @@ -9,19 +10,26 @@ import java.nio.file.Files.createTempDirectory import java.nio.file.Paths import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertTrue class GenerateTaskDslTest : TestBase() { override var temp: File = createTempDirectory(javaClass.simpleName).toFile() - private val defaultBuildGradle = """ + @DataProvider(name = "property_format_provider") + private fun propertyFormatProvider(): Array> = arrayOf( + arrayOf("STRING"), + arrayOf("FILE") + ) + + private fun defaultBuildGradle(format: PropertyFormat) = """ plugins { id 'org.openapi.generator' } openApiGenerate { generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - outputDir = file("build/kotlin").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} + outputDir = ${"build/kotlin".toPropertyReference(format)} apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -91,8 +99,9 @@ class GenerateTaskDslTest : TestBase() { ) } - @Test - fun `openApiGenerate should create an expected file structure from root directory config`() { + @Test(dataProvider = "property_format_provider") + fun `openApiGenerate should create an expected file structure from root directory config`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml"), "spec-2.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.1.yaml") @@ -105,8 +114,8 @@ class GenerateTaskDslTest : TestBase() { } openApiGenerate { generatorName = "kotlin" - inputSpecRootDirectory = file("specs").absolutePath - outputDir = file("build/kotlin").absolutePath + inputSpecRootDirectory = ${"specs".toPropertyReference(propertyFormat)} + outputDir = ${"build/kotlin".toPropertyReference(propertyFormat)} apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -160,13 +169,14 @@ class GenerateTaskDslTest : TestBase() { ) } - @Test - fun `openApiGenerate should create an expected file structure from DSL config`() { + @Test(dataProvider = "property_format_provider") + fun `openApiGenerate should create an expected file structure from DSL config`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") ) - withProject(defaultBuildGradle, projectFiles) + withProject(defaultBuildGradle(propertyFormat), projectFiles) // Act val result = GradleRunner.create() @@ -200,13 +210,14 @@ class GenerateTaskDslTest : TestBase() { "Expected a successful run, but found ${result.task(":openApiGenerate")?.outcome}") } - @Test - fun `openApiGenerate should not cleanup outputDir by default`() { + @Test(dataProvider = "property_format_provider") + fun `openApiGenerate should not cleanup outputDir by default`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") ) - withProject(defaultBuildGradle, projectFiles) + withProject(defaultBuildGradle(propertyFormat), projectFiles) val oldFile = File(temp, "build/kotlin/should-not-be-removed") oldFile.mkdirs() @@ -233,8 +244,9 @@ class GenerateTaskDslTest : TestBase() { ) } - @Test - fun `openApiGenerate should cleanup outputDir`() { + @Test(dataProvider = "property_format_provider") + fun `openApiGenerate should cleanup outputDir`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") @@ -246,8 +258,8 @@ class GenerateTaskDslTest : TestBase() { } openApiGenerate { generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - outputDir = file("build/kotlin").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(propertyFormat)} + outputDir = ${"build/kotlin".toPropertyReference(propertyFormat)} apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -285,8 +297,9 @@ class GenerateTaskDslTest : TestBase() { ) } - @Test - fun `should apply prefix & suffix config parameters`() { + @Test(dataProvider = "property_format_provider") + fun `should apply prefix & suffix config parameters`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") @@ -297,8 +310,8 @@ class GenerateTaskDslTest : TestBase() { } openApiGenerate { generatorName = "java" - inputSpec = file("spec.yaml").absolutePath - outputDir = file("build/java").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(propertyFormat)} + outputDir = ${"build/java".toPropertyReference(propertyFormat)} apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -334,13 +347,14 @@ class GenerateTaskDslTest : TestBase() { "Expected a successful run, but found ${result.task(":openApiGenerate")?.outcome}") } - @Test - fun `openApiGenerate should used up-to-date instead of regenerate`() { + @Test(dataProvider = "property_format_provider") + fun `openApiGenerate should used up-to-date instead of regenerate`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") ) - withProject(defaultBuildGradle, projectFiles) + withProject(defaultBuildGradle(propertyFormat), projectFiles) // Act val resultFirstRun = GradleRunner.create() @@ -359,13 +373,14 @@ class GenerateTaskDslTest : TestBase() { assertTrue(resultSecondRun.output.contains("Task :openApiGenerate UP-TO-DATE"), "Task of second run should be up-to-date") } - @Test - fun `openApiGenerate should use cache instead of regenerate`() { + @Test(dataProvider = "property_format_provider") + fun `openApiGenerate should use cache instead of regenerate`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") ) - withProject(defaultBuildGradle, projectFiles) + withProject(defaultBuildGradle(propertyFormat), projectFiles) // Act val resultFirstRun = GradleRunner.create() @@ -398,14 +413,15 @@ class GenerateTaskDslTest : TestBase() { assertTrue(resultThirdRun.output.contains("Skipping task ':openApiGenerate' as it is up-to-date."), "Task of third run should not require rebuild") } - @Test - fun `openApiValidate should fail on invalid spec`() { + @Test(dataProvider = "property_format_provider") + fun `openApiValidate should fail on invalid spec`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0-invalid-due-to-missing-info-attribute.yaml") ) - withProject(defaultBuildGradle, projectFiles) + withProject(defaultBuildGradle(propertyFormat), projectFiles) // Act val result = GradleRunner.create() @@ -420,8 +436,9 @@ class GenerateTaskDslTest : TestBase() { "Expected a failed run, but found ${result.task(":openApiValidate")?.outcome}") } - @Test - fun `openApiValidate should ok skip spec validation`() { + @Test(dataProvider = "property_format_provider") + fun `openApiValidate should ok skip spec validation`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0-invalid-due-to-missing-info-attribute.yaml") @@ -433,8 +450,8 @@ class GenerateTaskDslTest : TestBase() { } openApiGenerate { generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - outputDir = file("build/kotlin").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(propertyFormat)} + outputDir = ${"build/kotlin".toPropertyReference(propertyFormat)} apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -458,8 +475,9 @@ class GenerateTaskDslTest : TestBase() { "Expected a successful run, but found ${result.task(":openApiGenerate")?.outcome}") } - @Test - fun `openapiGenerate should attempt to set handlebars when specified as engine`() { + @Test(dataProvider = "property_format_provider") + fun `openapiGenerate should attempt to set handlebars when specified as engine`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") @@ -471,8 +489,8 @@ class GenerateTaskDslTest : TestBase() { } openApiGenerate { generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - outputDir = file("build/kotlin").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(propertyFormat)} + outputDir = ${"build/kotlin".toPropertyReference(propertyFormat)} apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -495,8 +513,9 @@ class GenerateTaskDslTest : TestBase() { "Expected a failed run, but found ${result.task(":openApiGenerate")?.outcome}") } - @Test - fun `openapiGenerate should attempt to set my-custom-engine (or any other) when specified as engine`() { + @Test(dataProvider = "property_format_provider") + fun `openapiGenerate should attempt to set my-custom-engine (or any other) when specified as engine`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") @@ -508,8 +527,8 @@ class GenerateTaskDslTest : TestBase() { } openApiGenerate { generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - outputDir = file("build/kotlin").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(propertyFormat)} + outputDir = ${"build/kotlin".toPropertyReference(propertyFormat)} apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -531,8 +550,9 @@ class GenerateTaskDslTest : TestBase() { "Expected a failed run, but found ${result.task(":openApiGenerate")?.outcome}") } - @Test - fun `openapiGenerate should set dryRun flag`() { + @Test(dataProvider = "property_format_provider") + fun `openapiGenerate should set dryRun flag`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") @@ -544,8 +564,8 @@ class GenerateTaskDslTest : TestBase() { } openApiGenerate { generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - outputDir = file("build/kotlin").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(propertyFormat)} + outputDir = ${"build/kotlin".toPropertyReference(propertyFormat)} apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -572,8 +592,9 @@ class GenerateTaskDslTest : TestBase() { ) } - @Test - fun `openapiGenerate should set openapiGeneratorIgnoreList option`() { + @Test(dataProvider = "property_format_provider") + fun `openapiGenerate should set openapiGeneratorIgnoreList option`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") @@ -585,8 +606,8 @@ class GenerateTaskDslTest : TestBase() { } openApiGenerate { generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - outputDir = file("build/kotlin").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(propertyFormat)} + outputDir = ${"build/kotlin".toPropertyReference(propertyFormat)} apiPackage = "org.openapitools.example.api" invokerPackage = "org.openapitools.example.invoker" modelPackage = "org.openapitools.example.model" @@ -622,4 +643,79 @@ class GenerateTaskDslTest : TestBase() { "Expected a successful run, but found ${result.task(":openApiGenerate")?.outcome}" ) } + + @DataProvider(name = "gradle_version_provider") + private fun gradleVersionProvider(): Array> = arrayOf( + arrayOf("8.14.4"), + arrayOf("8.5"), + ) + + @Test(dataProvider = "gradle_version_provider") + fun `test implicit task wiring from producer task to generator`(gradleVersion: String) { + // Build script with a producer task that creates a spec file at execution time + val buildContents = """ + plugins { + id 'org.openapi.generator' + } + + // A task that creates a spec file at execution time + abstract class SpecProducerTask extends DefaultTask { + @OutputFile + abstract RegularFileProperty getOutputFile() + + @TaskAction + void create() { + getOutputFile().get().asFile.text = ''' +openapi: 3.0.0 +info: + title: Produced API + version: 1.0.0 +paths: + /pets: + get: + responses: + '200': + description: Success +'''.stripIndent() + } + } + + // Register the producer + def producer = tasks.register("produceSpec", SpecProducerTask) { + outputFile = layout.buildDirectory.file("dynamic-spec.yaml") + } + + // Configure the generator with implicit wiring + openApiGenerate { + generatorName = "kotlin" + inputSpec.set(producer.flatMap { it.outputFile }) + outputDir = layout.buildDirectory.dir("generated") + apiPackage = "org.openapitools.example.api" + modelPackage = "org.openapitools.example.model" + } + """.trimIndent() + + File(temp, "build.gradle").writeText(buildContents) + + // Run the generator + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("openApiGenerate") + .withPluginClasspath() + .withGradleVersion(gradleVersion) + .build() + + // Verify: The producer task MUST have run because the generator needed its output + val producerTask = result.task(":produceSpec") + val generatorTask = result.task(":openApiGenerate") + + assertNotNull(producerTask, "Producer task should have been part of the graph") + assertEquals(TaskOutcome.SUCCESS, producerTask.outcome, "Producer task should have succeeded") + assertNotNull(generatorTask, "Generator task should have been part of the graph") + assertEquals(TaskOutcome.SUCCESS, generatorTask.outcome, "Generator task should have succeeded") + + // Check that the generator actually produced something + val versionFile = File(temp, "build/generated/.openapi-generator/VERSION") + assertTrue(versionFile.exists(), "Generator should have run and produced output") + } } diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskFromCacheTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskFromCacheTest.kt index 54ae76cb240f..fc6d8f33c77e 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskFromCacheTest.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskFromCacheTest.kt @@ -22,31 +22,38 @@ class GenerateTaskFromCacheTest : TestBase() { } @DataProvider(name = "gradle_version_provider") - private fun gradleVersionProvider(): Array> = arrayOf(arrayOf("8.14.4"), arrayOf("8.5")) + private fun gradleVersionProvider(): Array> = arrayOf( + arrayOf("8.14.4", "STRING"), + arrayOf("8.14.4", "FILE"), + arrayOf("8.5", "STRING"), + arrayOf("8.5", "FILE"), + ) // inputSpec tests - private val inputSpecExtensionContents = """ + private fun inputSpecExtensionContents(format: PropertyFormat) = """ generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} """.trimIndent() @Test(dataProvider = "gradle_version_provider") - fun `inputSpec - same directory - openApiGenerate task output should come from cache`(gradleVersion: String) { - runCacheabilityTestUsingSameDirectory(gradleVersion, inputSpecExtensionContents) + fun `inputSpec - same directory - openApiGenerate task output should come from cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) + runCacheabilityTestUsingSameDirectory(gradleVersion, inputSpecExtensionContents(propertyFormat)) } @Test(dataProvider = "gradle_version_provider") - fun `inputSpec - different directory - openApiGenerate task output should come from cache`(gradleVersion: String) { - runCacheabilityTestUsingDifferentDirectories(gradleVersion, inputSpecExtensionContents) + fun `inputSpec - different directory - openApiGenerate task output should come from cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) + runCacheabilityTestUsingDifferentDirectories(gradleVersion, inputSpecExtensionContents(propertyFormat)) } // templateDir tests - private val templateDirExtensionContents = """ + private fun templateDirExtensionContents(format: PropertyFormat) = """ generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - templateDir = file("templateDir").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} + templateDir = ${"templateDir".toPropertyReference(format)} """.trimIndent() private fun initializeTemplateDirTest() { @@ -54,23 +61,25 @@ class GenerateTaskFromCacheTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `templateDir - same directory - openApiGenerate task output should come from cache`(gradleVersion: String) { + fun `templateDir - same directory - openApiGenerate task output should come from cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) initializeTemplateDirTest() - runCacheabilityTestUsingSameDirectory(gradleVersion, templateDirExtensionContents) + runCacheabilityTestUsingSameDirectory(gradleVersion, templateDirExtensionContents(propertyFormat)) } @Test(dataProvider = "gradle_version_provider") - fun `templateDir - different directory - openApiGenerate task output should come from cache`(gradleVersion: String) { + fun `templateDir - different directory - openApiGenerate task output should come from cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) initializeTemplateDirTest() - runCacheabilityTestUsingDifferentDirectories(gradleVersion, templateDirExtensionContents) + runCacheabilityTestUsingDifferentDirectories(gradleVersion, templateDirExtensionContents(propertyFormat)) } // configFile tests - private val configFileExtensionContents = """ + private fun configFileExtensionContents(format: PropertyFormat) = """ generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - configFile = file("configFile").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} + configFile = ${"configFile".toPropertyReference(format)} """.trimIndent() private fun initializeConfigFileTest() { @@ -80,23 +89,25 @@ class GenerateTaskFromCacheTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `configFile - same directory - openApiGenerate task output should come from cache`(gradleVersion: String) { + fun `configFile - same directory - openApiGenerate task output should come from cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) initializeConfigFileTest() - runCacheabilityTestUsingSameDirectory(gradleVersion, configFileExtensionContents) + runCacheabilityTestUsingSameDirectory(gradleVersion, configFileExtensionContents(propertyFormat)) } @Test(dataProvider = "gradle_version_provider") - fun `configFile - different directory - openApiGenerate task output should come from cache`(gradleVersion: String) { + fun `configFile - different directory - openApiGenerate task output should come from cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) initializeConfigFileTest() - runCacheabilityTestUsingDifferentDirectories(gradleVersion, configFileExtensionContents) + runCacheabilityTestUsingDifferentDirectories(gradleVersion, configFileExtensionContents(propertyFormat)) } // ignoreFileOverride tests - private val ignoreFileOverrideExtensionContents = """ + private fun ignoreFileOverrideExtensionContents(format: PropertyFormat) = """ generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - ignoreFileOverride = file(".openapi-generator-ignore").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} + ignoreFileOverride = ${".openapi-generator-ignore".toPropertyReference(format)} """.trimIndent() private fun initializeIgnoreFileTest() { @@ -104,15 +115,17 @@ class GenerateTaskFromCacheTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `ignoreFileOverride - same directory - openApiGenerate task output should come from cache`(gradleVersion: String) { + fun `ignoreFileOverride - same directory - openApiGenerate task output should come from cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) initializeIgnoreFileTest() - runCacheabilityTestUsingSameDirectory(gradleVersion, ignoreFileOverrideExtensionContents) + runCacheabilityTestUsingSameDirectory(gradleVersion, ignoreFileOverrideExtensionContents(propertyFormat)) } @Test(dataProvider = "gradle_version_provider") - fun `ignoreFileOverride - different directory - openApiGenerate task output should come from cache`(gradleVersion: String) { + fun `ignoreFileOverride - different directory - openApiGenerate task output should come from cache`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) initializeIgnoreFileTest() - runCacheabilityTestUsingDifferentDirectories(gradleVersion, ignoreFileOverrideExtensionContents) + runCacheabilityTestUsingDifferentDirectories(gradleVersion, ignoreFileOverrideExtensionContents(propertyFormat)) } // Helper methods & test fixtures diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskUpToDateTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskUpToDateTest.kt index 730d5ba4b690..edd1bb9a0816 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskUpToDateTest.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskUpToDateTest.kt @@ -9,23 +9,30 @@ import kotlin.test.assertEquals class GenerateTaskUpToDateTest : TestBase() { @DataProvider(name = "gradle_version_provider") - private fun gradleVersionProvider(): Array> = arrayOf(arrayOf("8.14.4"), arrayOf("8.5")) + private fun gradleVersionProvider(): Array> = arrayOf( + arrayOf("8.14.4", "STRING"), + arrayOf("8.14.4", "FILE"), + arrayOf("8.5", "STRING"), + arrayOf("8.5", "FILE"), + ) // inputSpec tests - private val inputSpecExtensionContents = """ + private fun inputSpecExtensionContents(format: PropertyFormat) = """ generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} """.trimIndent() @Test(dataProvider = "gradle_version_provider") - fun `inputSpec - no file changes - should be up-to-date`(gradleVersion: String) { - runShouldBeUpToDateTest(gradleVersion, inputSpecExtensionContents) + fun `inputSpec - no file changes - should be up-to-date`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) + runShouldBeUpToDateTest(gradleVersion, inputSpecExtensionContents(propertyFormat)) } @Test(dataProvider = "gradle_version_provider") - fun `inputSpec - has file changes - should execute`(gradleVersion: String) { - runShouldExecuteTest(gradleVersion, inputSpecExtensionContents) { + fun `inputSpec - has file changes - should execute`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) + runShouldExecuteTest(gradleVersion, inputSpecExtensionContents(propertyFormat)) { val inputSpec = File(temp, "spec.yaml") val newContents = inputSpec.readText().replace("version: 1.0.0", "version: 1.0.1") inputSpec.writeText(newContents) @@ -34,10 +41,10 @@ class GenerateTaskUpToDateTest : TestBase() { // templateDir tests - private val templateDirExtensionContents = """ + private fun templateDirExtensionContents(format: PropertyFormat) = """ generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - templateDir = file("templateDir").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} + templateDir = ${"templateDir".toPropertyReference(format)} """.trimIndent() private fun initializeTemplateDirTest(): File { @@ -47,25 +54,27 @@ class GenerateTaskUpToDateTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `templateDir - no file changes - should be up-to-date`(gradleVersion: String) { + fun `templateDir - no file changes - should be up-to-date`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) initializeTemplateDirTest() - runShouldBeUpToDateTest(gradleVersion, templateDirExtensionContents) + runShouldBeUpToDateTest(gradleVersion, templateDirExtensionContents(propertyFormat)) } @Test(dataProvider = "gradle_version_provider") - fun `templateDir - has file changes - should execute`(gradleVersion: String) { + fun `templateDir - has file changes - should execute`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) val templateFile = initializeTemplateDirTest() - runShouldExecuteTest(gradleVersion, templateDirExtensionContents) { + runShouldExecuteTest(gradleVersion, templateDirExtensionContents(propertyFormat)) { templateFile.writeText("new contents") } } // configFile tests - private val configFileExtensionContents = """ + private fun configFileExtensionContents(format: PropertyFormat) = """ generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - configFile = file("configFile").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} + configFile = ${"configFile".toPropertyReference(format)} """.trimIndent() private fun initializeConfigFileTest(): File { @@ -73,25 +82,27 @@ class GenerateTaskUpToDateTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `configFile - no file changes - should be up-to-date`(gradleVersion: String) { + fun `configFile - no file changes - should be up-to-date`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) initializeConfigFileTest() - runShouldBeUpToDateTest(gradleVersion, configFileExtensionContents) + runShouldBeUpToDateTest(gradleVersion, configFileExtensionContents(propertyFormat)) } @Test(dataProvider = "gradle_version_provider") - fun `configFile - has file changes - should execute`(gradleVersion: String) { + fun `configFile - has file changes - should execute`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) val configFile = initializeConfigFileTest() - runShouldExecuteTest(gradleVersion, configFileExtensionContents) { + runShouldExecuteTest(gradleVersion, configFileExtensionContents(propertyFormat)) { configFile.writeText("""{"foo":"baz"}""") } } // ignoreFileOverride tests - private val ignoreFileOverrideExtensionContents = """ + private fun ignoreFileOverrideExtensionContents(format: PropertyFormat) = """ generatorName = "kotlin" - inputSpec = file("spec.yaml").absolutePath - ignoreFileOverride = file(".openapi-generator-ignore").absolutePath + inputSpec = ${"spec.yaml".toPropertyReference(format)} + ignoreFileOverride = ${".openapi-generator-ignore".toPropertyReference(format)} """.trimIndent() private fun initializeIgnoreFileTest(): File { @@ -99,15 +110,17 @@ class GenerateTaskUpToDateTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `ignoreFileOverride - no file changes - should be up-to-date`(gradleVersion: String) { + fun `ignoreFileOverride - no file changes - should be up-to-date`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) initializeIgnoreFileTest() - runShouldBeUpToDateTest(gradleVersion, ignoreFileOverrideExtensionContents) + runShouldBeUpToDateTest(gradleVersion, ignoreFileOverrideExtensionContents(propertyFormat)) } @Test(dataProvider = "gradle_version_provider") - fun `ignoreFileOverride - has file changes - should execute`(gradleVersion: String) { + fun `ignoreFileOverride - has file changes - should execute`(gradleVersion: String, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) val ignoreFileOverride = initializeIgnoreFileTest() - runShouldExecuteTest(gradleVersion, ignoreFileOverrideExtensionContents) { + runShouldExecuteTest(gradleVersion, ignoreFileOverrideExtensionContents(propertyFormat)) { ignoreFileOverride.writeText(".new_file_to_ignore") } } diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/MetaTaskDslTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/MetaTaskDslTest.kt index fcad60cca037..20ee801d2ffc 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/MetaTaskDslTest.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/MetaTaskDslTest.kt @@ -2,6 +2,7 @@ package org.openapitools.generator.gradle.plugin import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome +import org.testng.annotations.DataProvider import org.testng.annotations.Test import java.io.File import java.nio.file.Files.createTempDirectory @@ -11,9 +12,20 @@ import kotlin.test.assertTrue class MetaTaskDslTest : TestBase() { override var temp: File = createTempDirectory(javaClass.simpleName).toFile() - @Test - fun `openApiMeta should generate desired project contents`() { + @DataProvider(name = "property_format_provider") + private fun propertyFormatProvider(): Array> = arrayOf( + arrayOf("STRING"), + arrayOf("FILE") + ) + + @Test(dataProvider = "property_format_provider") + fun `openApiMeta should generate desired project contents`(format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange + val outputFolderSetting = when (propertyFormat) { + PropertyFormat.STRING -> """outputFolder = "${'$'}buildDir/meta"""" + PropertyFormat.FILE -> """outputFolder.set(file("${'$'}buildDir/meta"))""" + } withProject(""" | plugins { | id 'org.openapi.generator' @@ -22,7 +34,7 @@ class MetaTaskDslTest : TestBase() { | openApiMeta { | generatorName = "Sample" | packageName = "org.openapitools.example" - | outputFolder = layout.buildDirectory.dir("meta").get().asFile.path + | $outputFolderSetting | } """.trimMargin()) diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/TestBase.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/TestBase.kt index b7abfb5d7180..fc1cf194e3d3 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/TestBase.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/TestBase.kt @@ -49,4 +49,21 @@ abstract class TestBase { .forwardOutput() .apply(configure) .build()!! +} + +enum class PropertyFormat { + STRING, + FILE +} + +/** + * Converts a file path to a Gradle property reference based on format. + * @param format The property format to use + * @return Formatted property value (either file("path").absolutePath or file("path")) + */ +fun String.toPropertyReference(format: PropertyFormat): String { + return when (format) { + PropertyFormat.STRING -> """file("$this").absolutePath""" + PropertyFormat.FILE -> """file("$this")""" + } } \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/ValidateTaskDslTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/ValidateTaskDslTest.kt index ed4dd49b714d..028e504799d7 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/ValidateTaskDslTest.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/ValidateTaskDslTest.kt @@ -16,6 +16,14 @@ class ValidateTaskDslTest : TestBase() { @DataProvider(name = "gradle_version_provider") fun gradleVersionProvider(): Array> = arrayOf( + arrayOf(null, "STRING"), // uses the version of Gradle used to build the plugin itself + arrayOf(null, "FILE"), // uses the version of Gradle used to build the plugin itself + arrayOf("8.5", "STRING"), + arrayOf("8.5", "FILE") + ) + + @DataProvider(name = "gradle_version_only_provider") + fun gradleVersionOnlyProvider(): Array> = arrayOf( arrayOf(null), // uses the version of Gradle used to build the plugin itself arrayOf("8.5") ) @@ -30,7 +38,23 @@ class ValidateTaskDslTest : TestBase() { } } - @Test(dataProvider = "gradle_version_provider") + private fun inputSpecProperty(path: String, format: PropertyFormat, useSetMethod: Boolean = false): String { + return if (useSetMethod) { + // For .set() method on RegularFileProperty in task definitions + // Both formats use file() since RegularFileProperty expects a File/RegularFile + """inputSpec.set(file("$path"))""" + } else { + // For direct assignment in extension (openApiValidate block) + // STRING: uses setInputSpec(String) bridge method + // FILE: direct file() assignment (also works via Gradle's property conversion) + when (format) { + PropertyFormat.STRING -> """inputSpec = "$path"""" + PropertyFormat.FILE -> """inputSpec = file("$path")""" + } + } + } + + @Test(dataProvider = "gradle_version_only_provider") fun `openApiValidate should fail on non-file spec`(gradleVersion: String?) { // Arrange withProject( @@ -72,7 +96,8 @@ class ValidateTaskDslTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `openApiValidate should succeed on valid spec`(gradleVersion: String?) { + fun `openApiValidate should succeed on valid spec`(gradleVersion: String?, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") @@ -85,7 +110,7 @@ class ValidateTaskDslTest : TestBase() { | } | | openApiValidate { - | inputSpec = file("spec.yaml").absolutePath + | ${inputSpecProperty("spec.yaml", propertyFormat)} | } """.trimMargin(), projectFiles ) @@ -109,7 +134,8 @@ class ValidateTaskDslTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `openApiValidate should fail on invalid spec`(gradleVersion: String?) { + fun `openApiValidate should fail on invalid spec`(gradleVersion: String?, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0-invalid-due-to-missing-info-attribute.yaml") @@ -121,7 +147,7 @@ class ValidateTaskDslTest : TestBase() { | } | | openApiValidate { - | inputSpec = file('spec.yaml').absolutePath + | ${inputSpecProperty("spec.yaml", propertyFormat)} | } """.trimMargin(), projectFiles ) @@ -149,7 +175,8 @@ class ValidateTaskDslTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `openApiValidate should fail on invalid spec with duplicate 200 status code`(gradleVersion: String?) { + fun `openApiValidate should fail on invalid spec with duplicate 200 status code`(gradleVersion: String?, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0-invalid-due-to-duplicate-200-status-code.yaml") @@ -161,7 +188,7 @@ class ValidateTaskDslTest : TestBase() { | } | | openApiValidate { - | inputSpec = file('spec.yaml').absolutePath + | ${inputSpecProperty("spec.yaml", propertyFormat)} | } """.trimMargin(), projectFiles ) @@ -188,7 +215,7 @@ class ValidateTaskDslTest : TestBase() { ) } - @Test(dataProvider = "gradle_version_provider") + @Test(dataProvider = "gradle_version_only_provider") fun `validateGoodSpec as defined task should succeed on valid spec`(gradleVersion: String?) { // Arrange val projectFiles = mapOf( @@ -202,7 +229,7 @@ class ValidateTaskDslTest : TestBase() { | } | | task validateGoodSpec(type: org.openapitools.generator.gradle.plugin.tasks.ValidateTask) { - | inputSpec.set(file("spec.yaml").absolutePath) + | inputSpec.set(file("spec.yaml")) | } """.trimMargin(), projectFiles ) @@ -225,7 +252,7 @@ class ValidateTaskDslTest : TestBase() { ) } - @Test(dataProvider = "gradle_version_provider") + @Test(dataProvider = "gradle_version_only_provider") fun `validateBadSpec as defined task should fail on invalid spec`(gradleVersion: String?) { // Arrange val projectFiles = mapOf( @@ -238,7 +265,7 @@ class ValidateTaskDslTest : TestBase() { | } | | task validateBadSpec(type: org.openapitools.generator.gradle.plugin.tasks.ValidateTask) { - | inputSpec.set(file("spec.yaml").absolutePath) + | inputSpec.set(file("spec.yaml")) | } """.trimMargin(), projectFiles ) @@ -266,7 +293,8 @@ class ValidateTaskDslTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `openApiValidate should succeed with recommendations on valid spec`(gradleVersion: String?) { + fun `openApiValidate should succeed with recommendations on valid spec`(gradleVersion: String?, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0-recommend.yaml") @@ -280,7 +308,7 @@ class ValidateTaskDslTest : TestBase() { | } | | openApiValidate { - | inputSpec = file("spec.yaml").absolutePath + | ${inputSpecProperty("spec.yaml", propertyFormat)} | } """.trimMargin(), projectFiles ) @@ -308,7 +336,8 @@ class ValidateTaskDslTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `openApiValidate should fail with treatWarningsAsErrors on valid spec with warnings`(gradleVersion: String?) { + fun `openApiValidate should fail with treatWarningsAsErrors on valid spec with warnings`(gradleVersion: String?, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0-recommend.yaml") @@ -321,7 +350,7 @@ class ValidateTaskDslTest : TestBase() { | } | | openApiValidate { - | inputSpec = file("spec.yaml").absolutePath + | ${inputSpecProperty("spec.yaml", propertyFormat)} | treatWarningsAsErrors = true | } """.trimMargin(), projectFiles @@ -350,7 +379,8 @@ class ValidateTaskDslTest : TestBase() { } @Test(dataProvider = "gradle_version_provider") - fun `openApiValidate should succeed without recommendations on valid spec`(gradleVersion: String?) { + fun `openApiValidate should succeed without recommendations on valid spec`(gradleVersion: String?, format: String) { + val propertyFormat = PropertyFormat.valueOf(format) // Arrange val projectFiles = mapOf( "spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0-recommend.yaml") @@ -363,7 +393,7 @@ class ValidateTaskDslTest : TestBase() { | } | | openApiValidate { - | inputSpec = file("spec.yaml").absolutePath + | ${inputSpecProperty("spec.yaml", propertyFormat)} | recommend = false | } """.trimMargin(), projectFiles