Skip to content

Commit 71c8d9c

Browse files
PicazsooCopilot
andauthored
[GRADLE-PLUGIN] Add schemaLocations (ConfigurableFileCollection) and expose schema tracking via extension DSL (#24979)
* Add schemaLocations (ConfigurableFileCollection) and expose schema tracking via extension DSL - Add schemaLocations: ConfigurableFileCollection to GenerateTask, allowing tracking of multiple files/dirs/trees as up-to-date/cache inputs for \\\-referenced schemas, complementing the existing single-directory schemaLocation. - Fix schemaLocation/schemaLocations never being wired from OpenApiGeneratorGenerateExtension to GenerateTask (openApiGenerate { schemaLocation = ... } previously had no effect). - Switch schemaLocation's PathSensitivity from ABSOLUTE to RELATIVE for build-cache portability, matching sibling inputs (inputSpec, inputSpecFiles, templateDir). schemaLocations also uses RELATIVE. - Add setSchemaLocationsAsStrings(vararg paths: String) Groovy-friendly bridge on both GenerateTask and the extension, and setSchemaLocation(String) on the extension to enable Groovy \=\ assignment. - Document both properties in README.adoc with Kotlin/Groovy DSL examples. - Add tests: extension-to-task wiring, up-to-date/execute behavior, Groovy bridge methods, and configuration-cache compatibility (store/reuse across runs and after tracked schema file changes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix misleading test names and assertions for schemaLocation/schemaLocations wiring - Rename GenerateTaskConfigurationCacheTest case to reflect that the configuration cache entry is reused (not invalidated) when a tracked schema file changes. - Strengthen ParameterWiringRegressionTest schemaLocation/schemaLocations/ setSchemaLocationsAsStrings tests to assert UP_TO_DATE reuse and forced re-execution after a tracked file change, so removing the wiring would fail them. - Strengthen the equivalent GroovyBridgeMethodsTest for setSchemaLocationsAsStrings. - Clarify in README.adoc that setSchemaLocationsAsStrings replaces rather than appends to schemaLocations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: extract shared assertTrackedSchemaInput helper for schema wiring tests Deduplicate the repeated initial-SUCCESS/unchanged-UP_TO_DATE/modified-SUCCESS pattern across the schemaLocation, schemaLocations, and setSchemaLocationsAsStrings tests in ParameterWiringRegressionTest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "test: extract shared assertTrackedSchemaInput helper for schema wiring tests" This reverts commit 5666738. * Reapply "test: extract shared assertTrackedSchemaInput helper for schema wiring tests" This reverts commit 395e01a. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e2d02e8 commit 71c8d9c

8 files changed

Lines changed: 449 additions & 4 deletions

File tree

‎modules/openapi-generator-gradle-plugin/README.adoc‎

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,16 @@ apply plugin: 'org.openapi.generator'
160160
|None
161161
|Local root folder with spec file(s)
162162

163+
|schemaLocation
164+
|String / Provider<Directory>
165+
|None
166+
|Optional directory containing additional schema files referenced via `$ref` in the input specification. By default, Gradle only tracks `inputSpec` for up-to-date checks, so changes to `$ref`-referenced schema files elsewhere would not trigger re-generation. Declaring this directory makes Gradle track all files inside it too. This only affects up-to-date/cache tracking; it does not change how `$ref`s are resolved at generation time. For schemas that aren't all under one directory, use `schemaLocations` instead.
167+
168+
|schemaLocations
169+
|ConfigurableFileCollection
170+
|empty
171+
|Like `schemaLocation`, but accepts any combination of individual files, multiple directories, or filtered file trees instead of a single whole directory, e.g. `schemaLocations.from("schemas/user.yaml", "schemas/order.yaml")` or `schemaLocations.from(fileTree("schemas") { include("*.yaml") })`. Since it isn't a `Provider`-based property, it does not support Groovy `=` assignment — use `.from(...)`, or the `setSchemaLocationsAsStrings(vararg paths)` convenience method.
172+
163173
|mergedFileName
164174
|String / Provider<String>
165175
|`merged`
@@ -559,6 +569,47 @@ models: "User:Pet"
559569
----
560570
====
561571

572+
==== Tracking additional schema files ($ref)
573+
574+
By default, Gradle only watches `inputSpec` for up-to-date checks, so changes to `$ref`-referenced
575+
schema files elsewhere are not picked up automatically. Use `schemaLocation` (a single directory)
576+
or `schemaLocations` (any combination of files, directories, or filtered file trees) to have Gradle
577+
track them too. Neither property changes how `$ref`s are resolved at generation time — they only
578+
affect Gradle's up-to-date/cache tracking.
579+
580+
[source,kotlin]
581+
----
582+
openApiGenerate {
583+
inputSpec.set("$rootDir/specs/petstore.yaml")
584+
// Track a single directory of referenced schemas
585+
schemaLocation.set(file("$rootDir/specs/schemas"))
586+
// Or track specific files / multiple locations / filtered trees
587+
schemaLocations.from("specs/schemas/user.yaml", "specs/schemas/order.yaml")
588+
}
589+
----
590+
591+
[source,groovy]
592+
----
593+
openApiGenerate {
594+
inputSpec = "$rootDir/specs/petstore.yaml"
595+
schemaLocation = "specs/schemas"
596+
schemaLocations.from("specs/schemas/user.yaml", "specs/schemas/order.yaml")
597+
}
598+
----
599+
600+
Alternatively, the Groovy-friendly `setSchemaLocationsAsStrings(...)` helper can be used instead of
601+
`schemaLocations.from(...)` — note that it *replaces* any previously configured `schemaLocations`
602+
entries rather than adding to them, so do not combine the two on the same property:
603+
604+
[source,groovy]
605+
----
606+
openApiGenerate {
607+
inputSpec = "$rootDir/specs/petstore.yaml"
608+
schemaLocation = "specs/schemas"
609+
setSchemaLocationsAsStrings("specs/schemas/user.yaml", "specs/schemas/order.yaml")
610+
}
611+
----
612+
562613
=== openApiValidate
563614

564615
.Options
@@ -777,7 +828,7 @@ When configuring project extensions like `openApiGenerate { }`, `openApiValidate
777828
|Extension |Available Methods
778829

779830
|`openApiGenerate`
780-
|`setInputSpec(path)`, `setOutputDir(path)`, `setTemplateDir(path)`, `setConfigFile(path)`, `setIgnoreFileOverride(path)`, `setInputSpecRootDirectory(path)`
831+
|`setInputSpec(path)`, `setOutputDir(path)`, `setTemplateDir(path)`, `setConfigFile(path)`, `setIgnoreFileOverride(path)`, `setInputSpecRootDirectory(path)`, `setSchemaLocation(path)`, `setSchemaLocationsAsStrings(vararg paths)`
781832

782833
|`openApiValidate`
783834
|`setInputSpec(path)`
@@ -881,7 +932,7 @@ Both patterns work identically for String-based property configuration in Groovy
881932
|Task Type |Available Methods
882933

883934
|`GenerateTask`
884-
|`setInputSpecAsString(path)`, `setOutputDirAsString(path)`, `setTemplateDirAsString(path)`, `setConfigFileAsString(path)`, `setIgnoreFileOverrideAsString(path)`, `setInputSpecRootDirectoryAsString(path)`, `setSchemaLocationAsString(path)`
935+
|`setInputSpecAsString(path)`, `setOutputDirAsString(path)`, `setTemplateDirAsString(path)`, `setConfigFileAsString(path)`, `setIgnoreFileOverrideAsString(path)`, `setInputSpecRootDirectoryAsString(path)`, `setSchemaLocationAsString(path)`, `setSchemaLocationsAsStrings(vararg paths)`
885936

886937
|`ValidateTask`
887938
|`setInputSpecAsString(path)`

‎modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ class OpenApiGeneratorPlugin : Plugin<Project> {
115115
inputSpecRootDirectory.set(generate.inputSpecRootDirectory)
116116
inputSpecRootDirectorySkipMerge.set(generate.inputSpecRootDirectorySkipMerge)
117117
inputSpecFiles.from(generate.inputSpecFiles)
118+
schemaLocation.set(generate.schemaLocation)
119+
schemaLocations.from(generate.schemaLocations)
118120
mergedFileOutputDir.set(generate.mergedFileOutputDir)
119121
mergedFileName.set(generate.mergedFileName)
120122
mergedFileInfoName.set(generate.mergedFileInfoName)

‎modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,30 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) {
8585
*/
8686
val inputSpecFiles: ConfigurableFileCollection = project.objects.fileCollection()
8787

88+
/**
89+
* Optional directory containing additional schema files referenced via `$ref` in the input
90+
* specification, tracked for up-to-date checks.
91+
*
92+
* Declaring this directory tells Gradle to track all files inside it for up-to-date checks.
93+
* Without it, changes to `$ref`-referenced schemas will not trigger re-generation because
94+
* Gradle only watches [inputSpec] by default.
95+
*
96+
* For schemas that aren't all under one directory, use [schemaLocations] instead.
97+
*/
98+
val schemaLocation: DirectoryProperty = project.objects.directoryProperty()
99+
100+
/**
101+
* Optional collection of additional schema files/directories referenced via `$ref` in the input
102+
* specification, tracked for up-to-date checks.
103+
*
104+
* Unlike [schemaLocation], which only accepts a single whole directory, this accepts any
105+
* combination of individual files, multiple directories, or filtered file trees, e.g.:
106+
* ```kotlin
107+
* schemaLocations.from("schemas/user.yaml", "schemas/order.yaml")
108+
* ```
109+
*/
110+
val schemaLocations: ConfigurableFileCollection = project.objects.fileCollection()
111+
88112
/**
89113
* Directory where the merged spec file is written when [inputSpecFiles] is used.
90114
* Must be set when [inputSpecFiles] is non-empty.
@@ -641,6 +665,24 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) {
641665
ignoreFileOverride.set(project.layout.projectDirectory.file(path))
642666
}
643667

668+
/** Backwards-compatibility bridge for schemaLocation */
669+
fun setSchemaLocation(path: String) {
670+
schemaLocation.set(project.layout.projectDirectory.dir(path))
671+
}
672+
673+
/**
674+
* Groovy-compatible helper for schemaLocations.
675+
*
676+
* [schemaLocations] is a [ConfigurableFileCollection], which does not support Groovy `=`
677+
* assignment (it isn't a [org.gradle.api.provider.Property]). Use this method instead:
678+
* ```groovy
679+
* setSchemaLocationsAsStrings("schemas/user.yaml", "schemas/order.yaml")
680+
* ```
681+
*/
682+
fun setSchemaLocationsAsStrings(vararg paths: String) {
683+
schemaLocations.setFrom(paths.map { project.layout.projectDirectory.asFile.resolve(it) })
684+
}
685+
644686
// ========================================================================
645687
// Kotlin DSL extension functions for property setters
646688
// These allow Kotlin DSL users to call .set(String) on file/directory properties
@@ -674,6 +716,8 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) {
674716
setInputSpecRootDirectory(path)
675717
} else if (this === templateDir) {
676718
setTemplateDir(path)
719+
} else if (this === schemaLocation) {
720+
setSchemaLocation(path)
677721
} else {
678722
// Fallback for any other DirectoryProperty
679723
this.set(project.layout.projectDirectory.dir(path))

‎modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,12 +367,36 @@ abstract class GenerateTask : DefaultTask() {
367367
* Declaring this directory tells Gradle to track all files inside it for up-to-date checks.
368368
* Without it, changes to `$ref`-referenced schemas will not trigger re-generation because
369369
* Gradle only watches [inputSpec] by default.
370+
*
371+
* For schemas that aren't all under one directory (individual files, multiple directories, or a
372+
* filtered subset of a directory), use [schemaLocations] instead, which accepts any combination of
373+
* files, directories, and file trees.
370374
*/
371375
@get:Optional
372376
@get:InputDirectory
373-
@get:PathSensitive(PathSensitivity.ABSOLUTE)
377+
@get:PathSensitive(PathSensitivity.RELATIVE)
374378
abstract val schemaLocation: DirectoryProperty
375379

380+
/**
381+
* Optional collection of additional schema files/directories referenced via `$ref` in the input
382+
* specification, tracked for up-to-date checks.
383+
*
384+
* Unlike [schemaLocation], which only accepts a single whole directory, this property is a
385+
* [ConfigurableFileCollection] and can be populated with any combination of individual files,
386+
* multiple directories, or filtered file trees, e.g.:
387+
* ```kotlin
388+
* schemaLocations.from("schemas/user.yaml", "schemas/order.yaml")
389+
* schemaLocations.from(fileTree("schemas") { include("*.yaml") })
390+
* ```
391+
*
392+
* As with [schemaLocation], this only affects Gradle's up-to-date/cache tracking; it does not
393+
* change how `$ref`s are resolved at generation time.
394+
*/
395+
@get:InputFiles
396+
@get:Optional
397+
@get:PathSensitive(PathSensitivity.RELATIVE)
398+
val schemaLocations: ConfigurableFileCollection = project.objects.fileCollection()
399+
376400
/**
377401
* The output target directory into which code will be generated.
378402
*/
@@ -1357,4 +1381,17 @@ abstract class GenerateTask : DefaultTask() {
13571381
fun setSchemaLocationAsString(path: String) {
13581382
schemaLocation.set(layout.projectDirectory.dir(path))
13591383
}
1384+
1385+
/**
1386+
* Groovy-compatible helper for schemaLocations property.
1387+
*
1388+
* [schemaLocations] is a [ConfigurableFileCollection], which does not support Groovy `=`
1389+
* assignment (it isn't a [org.gradle.api.provider.Property]). Use this method instead:
1390+
* ```groovy
1391+
* setSchemaLocationsAsStrings("schemas/user.yaml", "schemas/order.yaml")
1392+
* ```
1393+
*/
1394+
fun setSchemaLocationsAsStrings(vararg paths: String) {
1395+
schemaLocations.setFrom(paths.map { layout.projectDirectory.asFile.resolve(it) })
1396+
}
13601397
}

‎modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskConfigurationCacheTest.kt‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,90 @@ class GenerateTaskConfigurationCacheTest : TestBase() {
104104
assertTrue(result3.output.contains("Configuration cache entry reused."))
105105
}
106106

107+
// schemaLocation / schemaLocations tests
108+
109+
private fun schemaLocationsExtensionContents(format: PropertyFormat) = """
110+
generatorName = "kotlin"
111+
inputSpec = ${"spec.yaml".toPropertyReference(format)}
112+
schemaLocation = ${"schemaDir".toPropertyReference(format)}
113+
schemaLocations.from(${"schemaLocationsDir/schema.yaml".toPropertyReference(format)})
114+
cleanupOutput.set(true)
115+
""".trimIndent()
116+
117+
@Test(dataProvider = "gradle_version_provider")
118+
fun `openApiGenerate with schemaLocation and schemaLocations should reuse configuration cache`(gradleVersion: String, format: String) {
119+
val propertyFormat = PropertyFormat.valueOf(format)
120+
// Arrange
121+
withProject(schemaLocationsExtensionContents(propertyFormat))
122+
projectDirCC.resolve("schemaDir").mkdir().also {
123+
projectDirCC.resolve("schemaDir/schema.yaml").writeText("type: object")
124+
}
125+
projectDirCC.resolve("schemaLocationsDir").mkdir().also {
126+
projectDirCC.resolve("schemaLocationsDir/schema.yaml").writeText("type: object")
127+
}
128+
129+
// Act
130+
val result1 = build {
131+
withProjectDir(projectDirCC)
132+
withArguments("--configuration-cache", "clean", "openApiGenerate")
133+
withGradleVersion(gradleVersion)
134+
}
135+
136+
val result2 = build {
137+
withProjectDir(projectDirCC)
138+
withArguments("--configuration-cache", "clean", "openApiGenerate")
139+
withGradleVersion(gradleVersion)
140+
}
141+
142+
// Assert
143+
assertEquals(TaskOutcome.SUCCESS, result1.task(":openApiGenerate")?.outcome)
144+
assertTrue(result1.output.contains("Configuration cache entry stored."))
145+
assertEquals(TaskOutcome.SUCCESS, result2.task(":openApiGenerate")?.outcome)
146+
assertTrue(result2.output.contains("Configuration cache entry reused."))
147+
}
148+
149+
@Test(dataProvider = "gradle_version_provider")
150+
fun `openApiGenerate with schemaLocation and schemaLocations should re-execute but reuse configuration cache on schema file change`(gradleVersion: String, format: String) {
151+
val propertyFormat = PropertyFormat.valueOf(format)
152+
// Arrange
153+
withProject(schemaLocationsExtensionContents(propertyFormat))
154+
projectDirCC.resolve("schemaDir").mkdir().also {
155+
projectDirCC.resolve("schemaDir/schema.yaml").writeText("type: object")
156+
}
157+
projectDirCC.resolve("schemaLocationsDir").mkdir().also {
158+
projectDirCC.resolve("schemaLocationsDir/schema.yaml").writeText("type: object")
159+
}
160+
161+
// Act - First run: store the configuration cache
162+
val result1 = build {
163+
withProjectDir(projectDirCC)
164+
withArguments("--configuration-cache", "openApiGenerate")
165+
withGradleVersion(gradleVersion)
166+
}
167+
assertEquals(TaskOutcome.SUCCESS, result1.task(":openApiGenerate")?.outcome)
168+
assertTrue(result1.output.contains("Configuration cache entry stored."))
169+
170+
// Act - Second run: change a file tracked via schemaLocation and re-run
171+
projectDirCC.resolve("schemaDir/schema.yaml").writeText("type: object\nadditionalProperties: false")
172+
val result2 = build {
173+
withProjectDir(projectDirCC)
174+
withArguments("--configuration-cache", "openApiGenerate")
175+
withGradleVersion(gradleVersion)
176+
}
177+
assertEquals(TaskOutcome.SUCCESS, result2.task(":openApiGenerate")?.outcome)
178+
assertTrue(result2.output.contains("Configuration cache entry reused."))
179+
180+
// Act - Third run: change a file tracked via schemaLocations and re-run
181+
projectDirCC.resolve("schemaLocationsDir/schema.yaml").writeText("type: object\nadditionalProperties: false")
182+
val result3 = build {
183+
withProjectDir(projectDirCC)
184+
withArguments("--configuration-cache", "openApiGenerate")
185+
withGradleVersion(gradleVersion)
186+
}
187+
assertEquals(TaskOutcome.SUCCESS, result3.task(":openApiGenerate")?.outcome)
188+
assertTrue(result3.output.contains("Configuration cache entry reused."))
189+
}
190+
107191
private fun getJavaVersion(): Int {
108192
val version = System.getProperty("java.version")
109193
val parts = version.split('.')

‎modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskUpToDateTest.kt‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,66 @@ class GenerateTaskUpToDateTest : TestBase() {
6767
}
6868
}
6969

70+
// schemaLocation tests
71+
72+
private fun schemaLocationExtensionContents(format: PropertyFormat) = """
73+
generatorName = "kotlin"
74+
inputSpec = ${"spec.yaml".toPropertyReference(format)}
75+
schemaLocation = ${"schemaDir".toPropertyReference(format)}
76+
""".trimIndent()
77+
78+
private fun initializeSchemaLocationTest(): File {
79+
val schemaDir = temp.resolve("schemaDir")
80+
schemaDir.mkdir()
81+
return schemaDir.resolve("schema.yaml").apply { writeText("type: object") }
82+
}
83+
84+
@Test(dataProvider = "gradle_version_provider")
85+
fun `schemaLocation - no file changes - should be up-to-date`(gradleVersion: String, format: String) {
86+
val propertyFormat = PropertyFormat.valueOf(format)
87+
initializeSchemaLocationTest()
88+
runShouldBeUpToDateTest(gradleVersion, schemaLocationExtensionContents(propertyFormat))
89+
}
90+
91+
@Test(dataProvider = "gradle_version_provider")
92+
fun `schemaLocation - has file changes - should execute`(gradleVersion: String, format: String) {
93+
val propertyFormat = PropertyFormat.valueOf(format)
94+
val schemaFile = initializeSchemaLocationTest()
95+
runShouldExecuteTest(gradleVersion, schemaLocationExtensionContents(propertyFormat)) {
96+
schemaFile.writeText("type: object\nadditionalProperties: false")
97+
}
98+
}
99+
100+
// schemaLocations tests
101+
102+
private fun schemaLocationsExtensionContents(format: PropertyFormat) = """
103+
generatorName = "kotlin"
104+
inputSpec = ${"spec.yaml".toPropertyReference(format)}
105+
schemaLocations.from(${"schemaLocationsDir/schema.yaml".toPropertyReference(format)})
106+
""".trimIndent()
107+
108+
private fun initializeSchemaLocationsTest(): File {
109+
val schemaDir = temp.resolve("schemaLocationsDir")
110+
schemaDir.mkdir()
111+
return schemaDir.resolve("schema.yaml").apply { writeText("type: object") }
112+
}
113+
114+
@Test(dataProvider = "gradle_version_provider")
115+
fun `schemaLocations - no file changes - should be up-to-date`(gradleVersion: String, format: String) {
116+
val propertyFormat = PropertyFormat.valueOf(format)
117+
initializeSchemaLocationsTest()
118+
runShouldBeUpToDateTest(gradleVersion, schemaLocationsExtensionContents(propertyFormat))
119+
}
120+
121+
@Test(dataProvider = "gradle_version_provider")
122+
fun `schemaLocations - has file changes - should execute`(gradleVersion: String, format: String) {
123+
val propertyFormat = PropertyFormat.valueOf(format)
124+
val schemaFile = initializeSchemaLocationsTest()
125+
runShouldExecuteTest(gradleVersion, schemaLocationsExtensionContents(propertyFormat)) {
126+
schemaFile.writeText("type: object\nadditionalProperties: false")
127+
}
128+
}
129+
70130
// configFile tests
71131

72132
private fun configFileExtensionContents(format: PropertyFormat) = """

0 commit comments

Comments
 (0)