diff --git a/modules/openapi-generator-gradle-plugin/README.adoc b/modules/openapi-generator-gradle-plugin/README.adoc index e6e7daa7c441..0dbfc731088a 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 @@ -680,6 +681,67 @@ Run with --stacktrace option to get the stack trace. Run with --info or --debug $ ./gradlew openApiValidate --input=/Users/jim/projects/openapi-generator/modules/openapi-generator/src/test/resources/3_0/petstore.yaml ---- +=== Groovy DSL Bridge Methods + +Both project extensions and custom task instances provide bridge methods that allow Groovy DSL users to set file and directory properties using simple String paths. These bridge methods support both **method-style** and **property-style** syntax. + +==== Extension Bridge Methods (for openApiGenerate, openApiValidate, openApiMeta) + +When configuring project extensions like `openApiGenerate { }`, `openApiValidate { }`, or `openApiMeta { }`, Groovy DSL users can use bridge methods to set properties with String paths. + +.Available Extension Bridge Methods +|=== +|Extension |Available Methods + +|`openApiGenerate` +|`setInputSpec(path)`, `setOutputDir(path)`, `setTemplateDir(path)`, `setConfigFile(path)`, `setIgnoreFileOverride(path)`, `setInputSpecRootDirectory(path)` + +|`openApiValidate` +|`setInputSpec(path)` + +|`openApiMeta` +|`setOutputFolder(path)` +|=== + +.Example: Using extension bridge methods in Groovy DSL +[source,groovy] +---- +// Method-style syntax +openApiGenerate { + generatorName.set("kotlin") + setInputSpec("$rootDir/specs/petstore.yaml") + setOutputDir("build/generated") + apiPackage.set("org.openapi.example.api") +} + +// Property-style syntax (Groovy allows this syntactic sugar) +openApiGenerate { + generatorName.set("kotlin") + inputSpec = "$rootDir/specs/petstore.yaml" + outputDir = "build/generated" + apiPackage.set("org.openapi.example.api") +} + +// Also works with openApiValidate +openApiValidate { + inputSpec = "$rootDir/specs/petstore.yaml" + recommend.set(true) +} + +// And openApiMeta +openApiMeta { + generatorName.set("MyGenerator") + outputFolder = "build/custom-generator" +} +---- + +[IMPORTANT] +==== +**Smart Input Routing:** The `setInputSpec()` method for extensions (and `setInputSpecAsString()` for custom tasks) automatically detects whether the provided path is a remote URL (e.g., `http://`, `https://`, `jar:`) or a local file path, and routes it to the appropriate property (`remoteInputSpec` or `inputSpec`). + +Additionally, when you set a local file path after previously setting a remote URL (or vice versa), the method automatically clears the conflicting property to prevent stale values from taking precedence. This ensures your configuration behaves predictably even when you change between local and remote specs. +==== + === Generate multiple sources If you want to perform multiple generation tasks, you'd want to create a task that inherits from the `GenerateTask`. @@ -715,6 +777,57 @@ task buildKotlinClient(type: org.openapitools.generator.gradle.plugin.tasks.Gene } ``` +==== Custom Task Bridge Methods (AsString suffix) + +When creating custom task instances (as shown above), Groovy DSL users also have access to bridge methods with the `AsString` suffix. These methods provide the same functionality as the extension bridge methods. + +[NOTE] +==== +**Why the "AsString" suffix?** + +Due to technical limitations with Gradle's abstract property getters in task classes, the bridge methods for custom tasks require the `AsString` suffix to avoid naming conflicts with Gradle's internal getter methods. + +- **Extensions** (e.g., `openApiGenerate { }`) use methods like `setInputSpec()`, `setOutputDir()` +- **Custom Tasks** (e.g., `task myTask(type: GenerateTask) { }`) use methods like `setInputSpecAsString()`, `setOutputDirAsString()` + +Both patterns work identically for String-based property configuration in Groovy DSL. +==== + +.Available AsString Bridge Methods for Custom Tasks +|=== +|Task Type |Available Methods + +|`GenerateTask` +|`setInputSpecAsString(path)`, `setOutputDirAsString(path)`, `setTemplateDirAsString(path)`, `setConfigFileAsString(path)`, `setIgnoreFileOverrideAsString(path)`, `setInputSpecRootDirectoryAsString(path)`, `setSchemaLocationAsString(path)` + +|`ValidateTask` +|`setInputSpecAsString(path)` + +|`MetaTask` +|`setOutputFolderAsString(path)` +|=== + +.Example: Using AsString methods in Groovy DSL +[source,groovy] +---- +// Method-style syntax +task buildCustomClient(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { + generatorName.set("kotlin") + setInputSpecAsString("$rootDir/specs/api.yaml") + setOutputDirAsString("build/generated/custom") + setTemplateDirAsString("templates/custom") + apiPackage.set("com.example.api") +} + +// Property-style syntax (Groovy allows this syntactic sugar) +task buildAnotherClient(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { + generatorName.set("java") + inputSpecAsString = "$rootDir/specs/another-api.yaml" + outputDirAsString = "build/generated/another" + modelPackage.set("com.example.model") +} +---- + To execute your specs, you'd then do: ``` @@ -742,6 +855,44 @@ 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] +==== +**Provider API vs Bridge Methods:** + +For optimal lazy configuration and task wiring, both Groovy and Kotlin DSL users should use `.set()` with the Provider API when possible (e.g., `inputSpec.set(downloadSpec.flatMap { it.outputFile })`). + +For simpler cases with static String paths, Groovy DSL users can use the convenient bridge methods: +- Extension configuration: Use methods like `setInputSpec()`, `setOutputDir()` (see "Extension Bridge Methods" section) +- Custom task configuration: Use methods like `setInputSpecAsString()`, `setOutputDirAsString()` (see "Custom Task Bridge Methods" section) +==== + == 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..6c2430a2c93b 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 { @@ -84,6 +84,7 @@ class OpenApiGeneratorPlugin : Plugin { description = "Validates an Open API 2.0 or 3.x specification document." inputSpec.set(validate.inputSpec) + remoteInputSpec.set(validate.remoteInputSpec) recommend.set(validate.recommend) treatWarningsAsErrors.set(validate.treatWarningsAsErrors) } 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..ec87d07f56e5 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,20 @@ 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 +import java.io.File /** * 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 +49,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 +58,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 +66,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 +83,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 +106,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 +169,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 +281,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 +415,103 @@ 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 DSL + // These allow Groovy users to use assignment syntax: inputSpec = "path" + // For Kotlin DSL, use the extension functions below instead. + // ======================================================================== + + /** 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) + inputSpec.set(null as File?) // Clear local file to prevent conflicts + } else { + inputSpec.set(project.layout.projectDirectory.file(path)) + remoteInputSpec.set(null as String?) // Clear remote URL to prevent conflicts + } + } + + /** 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)) + } + + // ======================================================================== + // Kotlin DSL extension functions for property setters + // These allow Kotlin DSL users to call .set(String) on file/directory properties + // ======================================================================== + + /** + * Extension function to allow setting inputSpec with a String path in Kotlin DSL. + * Example: inputSpec.set("$rootDir/api.yaml") + */ + fun RegularFileProperty.set(path: String) { + if (this === inputSpec) { + setInputSpec(path) + } else if (this === configFile) { + setConfigFile(path) + } else if (this === ignoreFileOverride) { + setIgnoreFileOverride(path) + } else { + // Fallback for any other RegularFileProperty + this.set(project.layout.projectDirectory.file(path)) + } + } + + /** + * Extension function to allow setting directory properties with a String path in Kotlin DSL. + * Example: outputDir.set("$buildDir/generated") + */ + fun DirectoryProperty.set(path: String) { + if (this === outputDir) { + setOutputDir(path) + } else if (this === inputSpecRootDirectory) { + setInputSpecRootDirectory(path) + } else if (this === templateDir) { + setTemplateDir(path) + } else { + // Fallback for any other DirectoryProperty + this.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/OpenApiGeneratorGeneratorsExtension.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGeneratorsExtension.kt index 8f91b1ea3420..03bd95cb454f 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 DSL + // This allows Groovy users to use assignment syntax: include = ... + // ======================================================================== + 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..7fbe77073785 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,56 @@ 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 DSL + // This allows Groovy users to use assignment syntax: outputFolder = "path" + // For Kotlin DSL, use the extension function below instead. + // ======================================================================== + + /** Backwards-compatibility bridge for outputFolder */ + fun setOutputFolder(path: String) { + outputFolder.set(project.layout.projectDirectory.dir(path)) + } + + // ======================================================================== + // Kotlin DSL extension function for property setter + // Allows Kotlin DSL users to call .set(String) on the outputFolder property + // ======================================================================== + + /** + * Extension function to allow setting outputFolder with a String path in Kotlin DSL. + * Example: outputFolder.set("$buildDir/generated") + */ + fun DirectoryProperty.set(path: String) { + if (this === outputFolder) { + setOutputFolder(path) + } else { + // Fallback for any other DirectoryProperty + this.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..f6a00c5668a4 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,70 @@ 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 +import java.io.File /** * 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 setter for Groovy DSL + // This allows Groovy users to use assignment syntax: inputSpec = "path" + // For Kotlin DSL, use the extension function below instead. + // ======================================================================== + + /** Backwards-compatibility bridge for inputSpec */ + fun setInputSpec(path: String) { + if (path.isRemoteUri()) { + remoteInputSpec.set(path) + inputSpec.set(null as File?) // Clear local file to prevent conflicts + } else { + inputSpec.set(project.layout.projectDirectory.file(path)) + remoteInputSpec.set(null as String?) // Clear remote URL to prevent conflicts + } + } + + // ======================================================================== + // Kotlin DSL extension function for property setter + // Allows Kotlin DSL users to call .set(String) on the inputSpec property + // ======================================================================== + + /** + * Extension function to allow setting inputSpec with a String path in Kotlin DSL. + * Example: inputSpec.set("$rootDir/api.yaml") + */ + fun RegularFileProperty.set(path: String) { + if (this === inputSpec) { + setInputSpec(path) + } else { + // Fallback for any other RegularFileProperty + this.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..2b9fd47e151d 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,245 @@ 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 java.io.File +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 +265,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 +310,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 +341,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 +352,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 +381,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 +396,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 +413,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 +484,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 +492,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 +661,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 +688,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 +699,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 +713,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 +725,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 +737,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 +749,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 +761,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 +778,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 +794,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 +816,225 @@ 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() + abstract val dryRun: 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() - - 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) - } - - 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) + 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.") } + } - skipValidateSpec.ifNotEmpty { value -> - configurator.setValidateSpec(!value) - } +// 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) + } + }) + } - generateAliasAsModel.ifNotEmpty { value -> - configurator.setGenerateAliasAsModel(value) - } + // ======================================================================== + // Kotlin DSL extension functions for property setters + // These allow Kotlin DSL users to call .set(String) on file/directory properties + // when configuring tasks directly (e.g., tasks.named("openApiGenerate") { ... }) + // ======================================================================== - engine.ifNotEmpty { value -> - if ("handlebars".equals(value, ignoreCase = true)) { - configurator.setTemplatingEngineName("handlebars") + /** + * Extension function to allow setting file properties with a String path in Kotlin DSL. + * Example: inputSpec.set("$rootDir/api.yaml") + */ + fun RegularFileProperty.set(path: String) { + when (this) { + inputSpec -> { + if (path.isRemoteUri()) { + remoteInputSpec.set(path) } 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) + this.set(layout.projectDirectory.file(path)) } } - - if (inlineSchemaNameMappings.isPresent) { - inlineSchemaNameMappings.get().forEach { entry -> - configurator.addInlineSchemaNameMapping(entry.key, entry.value) - } + configFile, ignoreFileOverride -> { + this.set(layout.projectDirectory.file(path)) } - - 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] - } - } + else -> { + // Fallback for any other RegularFileProperty + this.set(layout.projectDirectory.file(path)) } + } + } - try { - val out = services.get(StyledTextOutputFactory::class.java).create("openapi") - out.withStyle(StyledTextOutput.Style.Success) + /** + * Extension function to allow setting directory properties with a String path in Kotlin DSL. + * Example: outputDir.set("$buildDir/generated") + */ + fun DirectoryProperty.set(path: String) { + // All directory properties use the same conversion logic + this.set(layout.projectDirectory.dir(path)) + } - DefaultGenerator(dryRunSetting).opts(clientOptInput).generate() + // ======================================================================== + // Groovy DSL bridge methods + // These methods allow Groovy DSL users to set properties using String paths. + // Groovy's property syntax allows calling these as: + // - Method style: setInputSpecAsString("$rootDir/api.yaml") + // - Property style: inputSpecAsString = "$rootDir/api.yaml" + // ======================================================================== - out.println("Successfully generated code to ${outputDir.get()}") - } catch (e: RuntimeException) { - throw GradleException("Code generation failed.", e) - } - } finally { - GlobalSettings.reset() + /** + * Groovy-compatible setter for inputSpec property. + * Accepts a String and automatically routes to remote or local file based on URI detection. + * Clears the opposite property to prevent stale values from taking precedence. + */ + fun setInputSpecAsString(path: String) { + if (path.isRemoteUri()) { + remoteInputSpec.set(path) + inputSpec.set(null as File?) // Clear local file to prevent conflicts + } else { + inputSpec.set(layout.projectDirectory.file(path)) + remoteInputSpec.set(null as String?) // Clear remote URL to prevent conflicts } } -} -internal interface FileSystemManager { - - fun delete(outputDir: Property) + /** + * Groovy-compatible setter for configFile property. + */ + fun setConfigFileAsString(path: String) { + configFile.set(layout.projectDirectory.file(path)) + } -} + /** + * Groovy-compatible setter for ignoreFileOverride property. + */ + fun setIgnoreFileOverrideAsString(path: String) { + ignoreFileOverride.set(layout.projectDirectory.file(path)) + } -internal open class FileSystemManagerLegacy @Inject constructor(private val project: Project): FileSystemManager { + /** + * Groovy-compatible setter for templateDir property. + */ + fun setTemplateDirAsString(path: String) { + templateDir.set(layout.projectDirectory.dir(path)) + } - override fun delete(outputDir: Property) { - project.delete(outputDir) + /** + * Groovy-compatible setter for outputDir property. + */ + fun setOutputDirAsString(path: String) { + outputDir.set(layout.projectDirectory.dir(path)) } -} -internal open class FileSystemManagerDefault @Inject constructor(private val fs: FileSystemOperations) : FileSystemManager { + /** + * Groovy-compatible setter for inputSpecRootDirectory property. + */ + fun setInputSpecRootDirectoryAsString(path: String) { + inputSpecRootDirectory.set(layout.projectDirectory.dir(path)) + } - override fun delete(outputDir: Property) { - fs.delete { delete(outputDir) } + /** + * Groovy-compatible setter for schemaLocation property. + */ + fun setSchemaLocationAsString(path: String) { + schemaLocation.set(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/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..8a8fad7d81b0 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,13 @@ 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.file.ProjectLayout +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 @@ -36,6 +36,7 @@ import org.openapitools.codegen.templating.TemplateManagerOptions import java.io.File import java.io.IOException import java.nio.charset.Charset +import javax.inject.Inject /** * A task which generates a new generator (meta). Useful for redistributable generator packages. @@ -43,28 +44,28 @@ import java.nio.charset.Charset * @author Jim Schubert */ @CacheableTask -open class MetaTask : DefaultTask() { +abstract class MetaTask : DefaultTask() { + + @get:Inject + abstract val layout: ProjectLayout + @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 +84,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 +94,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 +122,52 @@ 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) -} + + // ======================================================================== + // Kotlin DSL extension function for property setter + // Allows Kotlin DSL users to call .set(String) on the outputFolder property + // when configuring tasks directly (e.g., tasks.named("openApiMeta") { ... }) + // ======================================================================== + + /** + * Extension function to allow setting outputFolder with a String path in Kotlin DSL. + * Example: outputFolder.set("$buildDir/generated") + */ + fun DirectoryProperty.set(path: String) { + // All directory properties use the same conversion logic + this.set(layout.projectDirectory.dir(path)) + } + + // ======================================================================== + // Groovy DSL bridge methods + // These methods allow Groovy DSL users to set properties using String paths. + // Groovy's property syntax allows calling these as: + // - Method style: setOutputFolderAsString("$buildDir/generated") + // - Property style: outputFolderAsString = "$buildDir/generated" + // ======================================================================== + + /** + * Groovy-compatible setter for outputFolder property. + */ + fun setOutputFolderAsString(path: String) { + outputFolder.set(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/tasks/ValidateTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/ValidateTask.kt index 4d2d13836541..c7f2085d8ad9 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,16 @@ 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 java.io.File +import javax.inject.Inject /** * A generator which validates an Open API spec. This task outputs a list of validation issues and errors. @@ -42,53 +37,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 + + init { + recommend.convention(true) + treatWarningsAsErrors.convention(false) + } - @get:Internal - @set:Option(option = "input", description = "The input specification.") - var input: String? = null - set(value) { - inputSpec.set(value) + @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 +114,90 @@ 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.") + } + + // ======================================================================== + // Kotlin DSL extension function for property setter + // Allows Kotlin DSL users to call .set(String) on the inputSpec property + // when configuring tasks directly (e.g., tasks.named("openApiValidate") { ... }) + // ======================================================================== + + /** + * Extension function to allow setting inputSpec with a String path in Kotlin DSL. + * Example: inputSpec.set("$rootDir/api.yaml") + */ + fun RegularFileProperty.set(path: String) { + when (this) { + inputSpec -> { + if (path.isRemoteUri()) { + remoteInputSpec.set(path) + } else { + this.set(layout.projectDirectory.file(path)) + } + } + else -> { + // Fallback for any other RegularFileProperty + this.set(layout.projectDirectory.file(path)) + } + } + } + + // ======================================================================== + // Groovy DSL bridge methods + // These methods allow Groovy DSL users to set properties using String paths. + // Groovy's property syntax allows calling these as: + // - Method style: setInputSpecAsString("$rootDir/api.yaml") + // - Property style: inputSpecAsString = "$rootDir/api.yaml" + // ======================================================================== + + /** + * Groovy-compatible setter for inputSpec property. + * Accepts a String and automatically routes to remote or local file based on URI detection. + * Clears the opposite property to prevent stale values from taking precedence. + */ + fun setInputSpecAsString(path: String) { + if (path.isRemoteUri()) { + remoteInputSpec.set(path) + inputSpec.set(null as File?) // Clear local file to prevent conflicts + } else { + inputSpec.set(layout.projectDirectory.file(path)) + remoteInputSpec.set(null as String?) // Clear remote URL to prevent conflicts + } } -} +} \ 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..7927fe0cc39b 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,269 @@ 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") + } + + @Test + fun `openApiGenerate should support Kotlin DSL set String syntax`() { + // Create a spec file + val specContent = """ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: Success + """.trimIndent() + File(temp, "spec.yaml").writeText(specContent) + + // Build script using Kotlin DSL with .set(String) syntax + val buildContents = """ + plugins { + id("org.openapi.generator") + } + + openApiGenerate { + generatorName.set("kotlin") + inputSpec.set("${'$'}projectDir/spec.yaml") + outputDir.set("${'$'}buildDir/generated-kotlin") + apiPackage.set("org.openapitools.example.api") + modelPackage.set("org.openapitools.example.model") + } + """.trimIndent() + + File(temp, "build.gradle.kts").writeText(buildContents) + + // Run the generator + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("openApiGenerate", "--stacktrace") + .withPluginClasspath() + .build() + + // Verify the task succeeded + assertEquals(TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome) + + // Verify output was generated + val versionFile = File(temp, "build/generated-kotlin/.openapi-generator/VERSION") + assertTrue(versionFile.exists(), "Generator should have produced output") + } + + @Test + fun `openApiGenerate should support Kotlin DSL set String syntax for all file and directory properties`() { + // Create a spec file and config file + val specContent = """ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: Success + """.trimIndent() + File(temp, "spec.yaml").writeText(specContent) + + val configContent = """ +{ + "dateLibrary": "java8" +} + """.trimIndent() + File(temp, "config.json").writeText(configContent) + + // Build script using Kotlin DSL with .set(String) syntax for multiple properties + val buildContents = """ + plugins { + id("org.openapi.generator") + } + + openApiGenerate { + generatorName.set("kotlin") + inputSpec.set("${'$'}projectDir/spec.yaml") + outputDir.set("${'$'}buildDir/generated") + configFile.set("${'$'}projectDir/config.json") + apiPackage.set("org.openapitools.example.api") + modelPackage.set("org.openapitools.example.model") + } + """.trimIndent() + + File(temp, "build.gradle.kts").writeText(buildContents) + + // Run the generator + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("openApiGenerate", "--stacktrace") + .withPluginClasspath() + .build() + + // Verify the task succeeded + assertEquals(TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome) + + // Verify output was generated + val versionFile = File(temp, "build/generated/.openapi-generator/VERSION") + assertTrue(versionFile.exists(), "Generator should have produced output") + } + + @Test + fun `openApiGenerate should support remote input spec with Kotlin DSL set String syntax`() { + // Build script using remote URL with .set(String) syntax + val specUrl = "https://raw.githubusercontent.com/OpenAPITools/openapi-generator/b6b8c0db872fb4a418ae496e89c7e656e14be165/modules/openapi-generator-gradle-plugin/src/test/resources/specs/petstore-v3.0.yaml" + val buildContents = """ + plugins { + id("org.openapi.generator") + } + + openApiGenerate { + generatorName.set("kotlin") + inputSpec.set("$specUrl") + outputDir.set("${'$'}buildDir/generated-remote") + apiPackage.set("org.openapitools.example.api") + modelPackage.set("org.openapitools.example.model") + } + """.trimIndent() + + File(temp, "build.gradle.kts").writeText(buildContents) + + // Run the generator + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("openApiGenerate", "--stacktrace") + .withPluginClasspath() + .build() + + // Verify the task succeeded + assertEquals(TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome) + + // Verify output was generated + val versionFile = File(temp, "build/generated-remote/.openapi-generator/VERSION") + assertTrue(versionFile.exists(), "Generator should have produced output from remote spec") + } + + @Test + fun `openApiGenerate should support task-level configuration with Kotlin DSL set String syntax`() { + // Create a spec file + val specContent = """ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: Success + """.trimIndent() + File(temp, "spec.yaml").writeText(specContent) + + // Build script using Kotlin DSL configuring task directly + val buildContents = """ + plugins { + id("org.openapi.generator") + } + + tasks.named("openApiGenerate") { + generatorName.set("kotlin") + inputSpec.set("${'$'}projectDir/spec.yaml") + outputDir.set("${'$'}buildDir/task-configured") + apiPackage.set("org.openapitools.example.api") + modelPackage.set("org.openapitools.example.model") + } + """.trimIndent() + + File(temp, "build.gradle.kts").writeText(buildContents) + + // Run the generator + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("openApiGenerate", "--stacktrace") + .withPluginClasspath() + .build() + + // Verify the task succeeded + assertEquals(TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome) + + // Verify output was generated + val versionFile = File(temp, "build/task-configured/.openapi-generator/VERSION") + assertTrue(versionFile.exists(), "Generator should have produced output with task-level configuration") + } } 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/GroovyBridgeMethodsTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GroovyBridgeMethodsTest.kt new file mode 100644 index 000000000000..6cd10c24060e --- /dev/null +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GroovyBridgeMethodsTest.kt @@ -0,0 +1,164 @@ +package org.openapitools.generator.gradle.plugin + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.testng.annotations.Test +import java.io.File +import java.nio.file.Files.createTempDirectory +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Test class for Groovy DSL bridge methods (setPropertyNameAsString pattern). + * These tests verify that Groovy users can use both method-style and property-style + * syntax to set file/directory properties using String paths. + * + * Also tests the fix for stale remoteInputSpec values. + */ +class GroovyBridgeMethodsTest : TestBase() { + override var temp: File = createTempDirectory(javaClass.simpleName).toFile() + + @Test + fun `Custom GenerateTask should accept all AsString bridge methods`() { + // Arrange + val buildContents = """ + plugins { + id 'org.openapi.generator' + } + + tasks.register('customGenerate', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { + generatorName = "kotlin" + setInputSpecAsString("spec.yaml") + setOutputDirAsString("build/custom-kotlin") + setTemplateDirAsString("templates") + apiPackage = "org.openapitools.custom.api" + invokerPackage = "org.openapitools.custom.invoker" + modelPackage = "org.openapitools.custom.model" + } + """.trimIndent() + + withProjectFiles(buildContents, includeTemplates = true) + + // Act + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("customGenerate") + .withPluginClasspath() + .build() + + // Assert + assertTrue( + result.output.contains("Successfully generated code to"), + "Expected successful generation in custom task using AsString methods" + ) + assertEquals( + TaskOutcome.SUCCESS, result.task(":customGenerate")?.outcome, + "Expected a successful run with custom task using AsString methods" + ) + } + + @Test + fun `Custom GenerateTask should accept all AsString bridge methods in groovy DSL`() { + // Arrange + val buildContents = """ + plugins { + id 'org.openapi.generator' + } + + tasks.register('customGenerate', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { + generatorName = "kotlin" + inputSpecAsString = "spec.yaml" + outputDirAsString = "build/custom-kotlin" + templateDirAsString = "templates" + apiPackage = "org.openapitools.custom.api" + invokerPackage = "org.openapitools.custom.invoker" + modelPackage = "org.openapitools.custom.model" + } + """.trimIndent() + + withProjectFiles(buildContents, includeTemplates = true) + + // Act + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("customGenerate") + .withPluginClasspath() + .build() + + // Assert + assertTrue( + result.output.contains("Successfully generated code to"), + "Expected successful generation in custom task using AsString methods" + ) + assertEquals( + TaskOutcome.SUCCESS, result.task(":customGenerate")?.outcome, + "Expected a successful run with custom task using AsString methods" + ) + } + + @Test + fun `setInputSpecAsString should clear stale remoteInputSpec when setting local file`() { + // This test verifies the P2 bug fix: setting a local file after a remote URL + // should clear the remote URL to prevent it from taking precedence + val buildContents = """ + plugins { + id 'org.openapi.generator' + } + + tasks.register('testGenerate', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { + generatorName = "kotlin" + // First set a remote URL (simulating previous configuration) + setInputSpecAsString("https://example.com/api.yaml") + // Then override with a local file - this should clear the remote URL + setInputSpecAsString("spec.yaml") + setOutputDirAsString("build/test-kotlin") + apiPackage = "org.openapitools.test.api" + invokerPackage = "org.openapitools.test.invoker" + modelPackage = "org.openapitools.test.model" + skipValidateSpec = true + } + """.trimIndent() + + withProjectFiles(buildContents) + + // Act + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("testGenerate") + .withPluginClasspath() + .build() + + // Assert - should use local file, not remote URL + assertTrue( + result.output.contains("Successfully generated code to"), + "Expected successful generation using local file (not remote URL)" + ) + assertEquals( + TaskOutcome.SUCCESS, result.task(":testGenerate")?.outcome, + "Expected a successful run with local file overriding remote URL" + ) + } + + // Helper method to create project files + private fun withProjectFiles(buildContents: String, includeConfig: Boolean = false, includeTemplates: Boolean = false) { + File(temp, "build.gradle").writeText(buildContents) + + // Create spec file + val specContent = javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml") + val specFile = File(temp, "spec.yaml") + specContent?.copyTo(specFile.outputStream()) + + // Create config file if needed + if (includeConfig) { + val configFile = File(temp, "config.json") + configFile.writeText("{}") + } + + // Create templates directory if needed + if (includeTemplates) { + val templatesDir = File(temp, "templates") + templatesDir.mkdirs() + } + } +} + 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..e86dd5022388 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()) @@ -55,4 +67,80 @@ class MetaTaskDslTest : TestBase() { "Expected a successful run, but found ${result.task(":openApiMeta")?.outcome}" ) } + + @Test + fun `openApiMeta should support Kotlin DSL set String syntax`() { + // Build script using Kotlin DSL with .set(String) syntax + val buildContents = """ + plugins { + id("org.openapi.generator") + } + + openApiMeta { + generatorName.set("KotlinTest") + packageName.set("org.openapitools.example.kotlin") + outputFolder.set("${'$'}buildDir/meta-kotlin") + } + """.trimIndent() + + File(temp, "build.gradle.kts").writeText(buildContents) + + // Run the meta task + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("openApiMeta", "--stacktrace") + .withPluginClasspath() + .build() + + // Assert + assertTrue(result.output.contains("Wrote file to"), "User-friendly write notice is missing.") + assertTrue(result.output.contains("KotlinTestGenerator.java"), "Expected generator class file") + assertEquals( + TaskOutcome.SUCCESS, + result.task(":openApiMeta")?.outcome, + "Expected a successful run" + ) + + // Verify the generator was created in the right location + val generatorFile = File(temp, "build/meta-kotlin/src/main/java/org/openapitools/example/kotlin/KotlinTestGenerator.java") + assertTrue(generatorFile.exists(), "Generator file should have been created") + } + + @Test + fun `openApiMeta should support task-level configuration with Kotlin DSL set String syntax`() { + // Build script using Kotlin DSL configuring task directly + val buildContents = """ + plugins { + id("org.openapi.generator") + } + + tasks.named("openApiMeta") { + generatorName.set("DirectTask") + packageName.set("org.openapitools.example.direct") + outputFolder.set("${'$'}buildDir/meta-direct") + } + """.trimIndent() + + File(temp, "build.gradle.kts").writeText(buildContents) + + // Run the meta task + val result = GradleRunner.create() + .withProjectDir(temp) + .withArguments("openApiMeta", "--stacktrace") + .withPluginClasspath() + .build() + + // Assert + assertTrue(result.output.contains("Wrote file to"), "User-friendly write notice is missing.") + assertTrue(result.output.contains("DirectTaskGenerator.java"), "Expected generator class file") + assertEquals( + TaskOutcome.SUCCESS, + result.task(":openApiMeta")?.outcome, + "Expected a successful run with task-level configuration" + ) + + // Verify the generator was created in the right location + val generatorFile = File(temp, "build/meta-direct/src/main/java/org/openapitools/example/direct/DirectTaskGenerator.java") + assertTrue(generatorFile.exists(), "Generator file should have been created with task-level configuration") + } } \ No newline at end of file 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..fb9073a03588 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 @@ -390,4 +420,135 @@ class ValidateTaskDslTest : TestBase() { "Expected a successful run, but found ${result.task(":openApiValidate")?.outcome}" ) } + + @Test(dataProvider = "gradle_version_only_provider") + fun `openApiValidate should support Kotlin DSL set String syntax in extension`(gradleVersion: String?) { + // Create a valid spec file + val specContent = """ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: Success + """.trimIndent() + File(temp, "spec.yaml").writeText(specContent) + + // Build script using Kotlin DSL with .set(String) syntax + val buildContents = """ + plugins { + id("org.openapi.generator") + } + + openApiValidate { + inputSpec.set("${'$'}projectDir/spec.yaml") + } + """.trimIndent() + + File(temp, "build.gradle.kts").writeText(buildContents) + + // Run validation + val result = getGradleRunner(gradleVersion) + .withProjectDir(temp) + .withArguments("openApiValidate") + .withPluginClasspath() + .build() + + // Assert + assertTrue( + result.output.contains("Spec is valid."), + "Expected validation success message" + ) + assertEquals( + SUCCESS, result.task(":openApiValidate")?.outcome, + "Expected a successful run" + ) + } + + @Test(dataProvider = "gradle_version_only_provider") + fun `openApiValidate should support Kotlin DSL set String syntax for remote spec`(gradleVersion: String?) { + // Build script using remote URL with .set(String) syntax + val specUrl = "https://raw.githubusercontent.com/OpenAPITools/openapi-generator/b6b8c0db872fb4a418ae496e89c7e656e14be165/modules/openapi-generator-gradle-plugin/src/test/resources/specs/petstore-v3.0.yaml" + val buildContents = """ + plugins { + id("org.openapi.generator") + } + + openApiValidate { + inputSpec.set("$specUrl") + } + """.trimIndent() + + File(temp, "build.gradle.kts").writeText(buildContents) + + // Run validation + val result = getGradleRunner(gradleVersion) + .withProjectDir(temp) + .withArguments("openApiValidate") + .withPluginClasspath() + .build() + + // Assert + assertTrue( + result.output.contains("Spec is valid."), + "Expected validation success message for remote spec" + ) + assertEquals( + SUCCESS, result.task(":openApiValidate")?.outcome, + "Expected a successful run" + ) + } + + @Test(dataProvider = "gradle_version_only_provider") + fun `openApiValidate should support task-level configuration with Kotlin DSL set String syntax`(gradleVersion: String?) { + // Create a valid spec file + val specContent = """ +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: Success + """.trimIndent() + File(temp, "spec.yaml").writeText(specContent) + + // Build script using Kotlin DSL configuring task directly + val buildContents = """ + plugins { + id("org.openapi.generator") + } + + tasks.named("openApiValidate") { + inputSpec.set("${'$'}projectDir/spec.yaml") + recommend.set(false) + } + """.trimIndent() + + File(temp, "build.gradle.kts").writeText(buildContents) + + // Run validation + val result = getGradleRunner(gradleVersion) + .withProjectDir(temp) + .withArguments("openApiValidate") + .withPluginClasspath() + .build() + + // Assert + assertTrue( + result.output.contains("Spec is valid."), + "Expected validation success message with task-level configuration" + ) + assertEquals( + SUCCESS, result.task(":openApiValidate")?.outcome, + "Expected a successful run" + ) + } }