Fix invalid prefix when generating spring boot config (#1862)

Fix invalid prefix when generating spring boot config

In Spring Boot, the prefix passed to `@ConfigurationProperties` must
be in "canonical form".

The presence of this annotation allows spring boot to directly inject
properties into a class, but is not actually needed.

The correct behavior here is to omit this annotation if the property
name does not match Spring Boot's canonical form.
This commit is contained in:
Daniel Chao
2026-09-11 15:49:04 -07:00
committed by GitHub
parent 9b52a4fd82
commit e3881f6149
6 changed files with 173 additions and 3 deletions
@@ -36,6 +36,7 @@ import kotlin.let
import kotlin.takeIf import kotlin.takeIf
import kotlin.to import kotlin.to
import org.pkl.commons.NameMapper import org.pkl.commons.NameMapper
import org.pkl.commons.isValidConfigurationPropertiesPrefix
import org.pkl.core.* import org.pkl.core.*
import org.pkl.core.util.CodeGeneratorUtils import org.pkl.core.util.CodeGeneratorUtils
import org.pkl.core.util.IoUtils import org.pkl.core.util.IoUtils
@@ -540,7 +541,11 @@ class JavaCodeGenerator(
} }
propertyType is PType.Class && propertyType.pClass == pClass propertyType is PType.Class && propertyType.pClass == pClass
} }
if (modulePropertiesWithMatchingType.size == 1) {
val singleProperty = modulePropertiesWithMatchingType.singleOrNull()
if (
singleProperty != null && singleProperty.simpleName.isValidConfigurationPropertiesPrefix
) {
// exactly one module property has this type -> make it available for direct injection // exactly one module property has this type -> make it available for direct injection
// (potential improvement: make type available for direct injection if it occurs exactly // (potential improvement: make type available for direct injection if it occurs exactly
// once in property tree) // once in property tree)
@@ -1555,6 +1555,56 @@ class JavaCodeGeneratorTest {
assertThat(javaCodeWithoutSpringAnnotations).compilesSuccessfully() assertThat(javaCodeWithoutSpringAnnotations).compilesSuccessfully()
} }
@Test
fun `spring boot -- single module property has type but invalid prefix`() {
val javaCode =
generateJavaCode(
"""
module my.mod
fooBar: FooBar
class FooBar
"""
.trimIndent(),
JavaCodeGeneratorOptions(generateSpringBootConfig = true),
)
// "fooBar" is not a valid prefix for `@ConfigurationProperties` annotations, so no
// annotation is added
assertThat(javaCode)
.contains(
"""
|
| public static final class FooBar {
"""
.trimMargin()
)
}
@Test
fun `spring boot -- single module property has type and valid prefix`() {
val javaCode =
generateJavaCode(
"""
module my.mod
server: Server
class Server
"""
.trimIndent(),
JavaCodeGeneratorOptions(generateSpringBootConfig = true),
)
assertThat(javaCode)
.contains(
"""
| @ConfigurationProperties("server")
| public static final class Server {
"""
.trimMargin()
)
}
@Test @Test
fun `import module`() { fun `import module`() {
val library = val library =
@@ -22,6 +22,7 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import java.io.StringWriter import java.io.StringWriter
import java.util.* import java.util.*
import org.pkl.commons.NameMapper import org.pkl.commons.NameMapper
import org.pkl.commons.isValidConfigurationPropertiesPrefix
import org.pkl.core.* import org.pkl.core.*
import org.pkl.core.util.CodeGeneratorUtils import org.pkl.core.util.CodeGeneratorUtils
import org.pkl.core.util.IoUtils import org.pkl.core.util.IoUtils
@@ -437,7 +438,10 @@ class KotlinCodeGenerator(
} }
propertyType is PType.Class && propertyType.pClass == pClass propertyType is PType.Class && propertyType.pClass == pClass
} }
if (modulePropertiesWithMatchingType.size == 1) { val singleProperty = modulePropertiesWithMatchingType.singleOrNull()
if (
singleProperty != null && singleProperty.simpleName.isValidConfigurationPropertiesPrefix
) {
// exactly one module property has this type -> make it available for direct injection // exactly one module property has this type -> make it available for direct injection
// (potential improvement: make type available for direct injection if it occurs exactly // (potential improvement: make type available for direct injection if it occurs exactly
// once in property tree) // once in property tree)
@@ -1470,6 +1470,56 @@ class KotlinCodeGeneratorTest {
assertThat(kotlinCodeWithoutSpringAnnotations).compilesSuccessfully() assertThat(kotlinCodeWithoutSpringAnnotations).compilesSuccessfully()
} }
@Test
fun `spring boot -- single module property has type but invalid prefix`() {
val javaCode =
generateKotlinCode(
"""
module my.mod
fooBar: FooBar
class FooBar
"""
.trimIndent(),
generateSpringBootConfig = true,
)
// "fooBar" is not a valid prefix for `@ConfigurationProperties` annotations, so no
// annotation is added
assertThat(javaCode)
.contains(
"""
|
| data class FooBar
"""
.trimMargin()
)
}
@Test
fun `spring boot -- single module property has type and valid prefix`() {
val javaCode =
generateKotlinCode(
"""
module my.mod
server: Server
class Server
"""
.trimIndent(),
generateSpringBootConfig = true,
)
assertThat(javaCode)
.contains(
"""
| @ConfigurationProperties("server")
| data class Server
"""
.trimMargin()
)
}
@Test @Test
fun `import module`() { fun `import module`() {
val library = val library =
@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -86,3 +86,21 @@ fun shlex(input: String): List<String> {
return result return result
} }
private val springPrefixRegex =
Regex(
"""
(?mx)
^
[a-z] # starts with lowercase letter
[a-z0-9]* # followed by zero or more lowercase letters or digits
(?:-[a-z0-9]+)* # followed by possibly kebab-cased
(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)* # followed by dot-separated nested prefixes
$
"""
.trimIndent()
)
/** Tells if this string is a valid prefix in Spring Boot's `@ConfigurationProperties` annotation */
val String.isValidConfigurationPropertiesPrefix: Boolean
get() = matches(springPrefixRegex)
@@ -0,0 +1,43 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.commons
import org.junit.jupiter.api.Test
class StringsTest {
@Test
fun isValidConfigurationPropertiesPrefix() {
val passes = listOf("app.datasource", "my-app.service-config", "app2.v1-api")
val negatives =
listOf(
"myApp.service",
"my_app.service",
"1app.service",
"app..service",
".app.service / app.",
)
for (value in passes) {
assert(value.isValidConfigurationPropertiesPrefix) {
"$value should have been valid but was not"
}
}
for (value in negatives) {
assert(!value.isValidConfigurationPropertiesPrefix) {
"$value should not have been valid but was"
}
}
}
}