Add support for customizing spring boot ConfigurationProperties (#1897)
This adds an option which allows users to control exactly how `@ConfigurationProperties` annotations are attached to classes. This helps when: * Spring Boot users don't have a single root class, but multiple classes that are injected in disparate places * Pkl's module structure doesn't match the resulting `.properties` layout
This commit is contained in:
1 parent
f973fd6717
commit
88a50d167e
15 files changed
+294
-10
No files matched your search
@@ -22,6 +22,24 @@ Default: (not set) +
|
||||
Flag that indicates to generate config classes for use with Spring Boot.
|
||||
====
|
||||
|
||||
.--spring-boot-configuration-properties
|
||||
[%collapsible]
|
||||
====
|
||||
Default: (none) +
|
||||
Example: `my.mod#Server=server` +
|
||||
Explicitly configures which generated classes receive the Spring Boot `@ConfigurationProperties` annotation, and with what prefix.
|
||||
Only applies if `--generate-spring-boot` is set.
|
||||
|
||||
The key is the Pkl class name (for example, `my.mod` for a module class, or `my.mod#Server` for a class within a module), and the value is the annotation's prefix.
|
||||
An empty value generates a bare `@ConfigurationProperties` annotation.
|
||||
|
||||
If this option is omitted, the annotations are inferred from the structure of the input modules.
|
||||
If this option is set, no annotations are inferred, and only the specified classes are annotated.
|
||||
|
||||
Repeat this option to define multiple mappings.
|
||||
Values must be valid Spring Boot configuration properties prefixes.
|
||||
====
|
||||
|
||||
.--implement-serializable
|
||||
[%collapsible]
|
||||
====
|
||||
|
||||
@@ -36,6 +36,21 @@ Example: `generateSpringBootConfig = true` +
|
||||
Whether to generate config classes for use with Spring Boot.
|
||||
====
|
||||
|
||||
.springBootConfigurationProperties: MapProperty<String, String>
|
||||
[%collapsible]
|
||||
====
|
||||
Default: `[:]` +
|
||||
Example: `springBootConfigurationProperties = ["my.mod": "", "my.mod#Server": "server"]` +
|
||||
Explicitly configures which generated classes receive the Spring Boot `@ConfigurationProperties` annotation, and with what prefix.
|
||||
Only applies if `generateSpringBootConfig` is `true`.
|
||||
|
||||
The key is the Pkl class name (for example, `my.mod` for a module class, or `my.mod#Server` for a class within a module), and the value is the annotation's prefix.
|
||||
An empty value generates a bare `@ConfigurationProperties` annotation.
|
||||
|
||||
If this property is empty, the annotations are inferred from the structure of the input modules.
|
||||
If this property is set, no annotations are inferred, and only the specified classes are annotated.
|
||||
====
|
||||
|
||||
.implementSerializable: Property<Boolean>
|
||||
[%collapsible]
|
||||
====
|
||||
|
||||
+12
-1
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -77,7 +77,17 @@ data class CliJavaCodeGeneratorOptions(
|
||||
* Pkl module name, and the value is the desired replacement.
|
||||
*/
|
||||
val renames: Map<String, String> = emptyMap(),
|
||||
|
||||
/**
|
||||
* The mapping of Pkl class names to the spring boot `@ConfigurationProperties` prefix name, when
|
||||
* [generateSpringBootConfig] is enabled.
|
||||
*
|
||||
* Setting this field is optional, and by default is inferred through the schema of the input
|
||||
* modules.
|
||||
*/
|
||||
val springBootConfigurationProperties: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("deprecated without replacement")
|
||||
fun toJavaCodegenOptions() = toJavaCodeGeneratorOptions()
|
||||
@@ -93,5 +103,6 @@ data class CliJavaCodeGeneratorOptions(
|
||||
nonNullAnnotation,
|
||||
implementSerializable,
|
||||
renames,
|
||||
springBootConfigurationProperties,
|
||||
)
|
||||
}
|
||||
@@ -97,7 +97,24 @@ data class JavaCodeGeneratorOptions(
|
||||
* from the corresponding name derived from the Pkl module declaration .
|
||||
*/
|
||||
val renames: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
/**
|
||||
* The mapping of Pkl class names to the spring boot `@ConfigurationProperties` prefix name, when
|
||||
* [generateSpringBootConfig] is enabled.
|
||||
*
|
||||
* Setting this field is optional, and by default is inferred through the schema of the input
|
||||
* modules.
|
||||
*/
|
||||
val springBootConfigurationProperties: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
init {
|
||||
for (value in springBootConfigurationProperties.values) {
|
||||
require(value.isEmpty() || value.isValidConfigurationPropertiesPrefix) {
|
||||
"Invalid value in `springBootConfigurationProperties`: expected a valid ConfigurationProperties prefix, but got '$value'."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Entrypoint for the Java code generator API. */
|
||||
class JavaCodeGenerator(
|
||||
@@ -523,7 +540,25 @@ class JavaCodeGenerator(
|
||||
}
|
||||
|
||||
fun generateSpringBootAnnotations(builder: TypeSpec.Builder) {
|
||||
if (isModuleClass) {
|
||||
if (codegenOptions.springBootConfigurationProperties.isNotEmpty()) {
|
||||
val configurationPropertyPrefix =
|
||||
codegenOptions.springBootConfigurationProperties[pClass.displayName] ?: return
|
||||
val annotation =
|
||||
with(
|
||||
AnnotationSpec.builder(
|
||||
ClassName.get(
|
||||
"org.springframework.boot.context.properties",
|
||||
"ConfigurationProperties",
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (!configurationPropertyPrefix.isEmpty()) {
|
||||
addMember("value", "\$S", configurationPropertyPrefix)
|
||||
}
|
||||
build()
|
||||
}
|
||||
builder.addAnnotation(annotation)
|
||||
} else if (isModuleClass) {
|
||||
builder.addAnnotation(
|
||||
ClassName.get("org.springframework.boot.context.properties", "ConfigurationProperties")
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.pkl.commons.cli.CliBaseOptions
|
||||
import org.pkl.commons.cli.cliMain
|
||||
import org.pkl.commons.cli.commands.ModulesCommand
|
||||
import org.pkl.commons.cli.commands.installCommonOptions
|
||||
import org.pkl.commons.isValidConfigurationPropertiesPrefix
|
||||
import org.pkl.commons.toPath
|
||||
import org.pkl.core.Release
|
||||
|
||||
@@ -131,6 +132,29 @@ class PklJavaCodegenCommand : ModulesCommand(name = "pkl-codegen-java", helpLink
|
||||
)
|
||||
.associate()
|
||||
|
||||
private val springBootConfigurationProperties: Map<String, String> by
|
||||
option(
|
||||
names = arrayOf("--spring-boot-configuration-properties"),
|
||||
metavar = "pkl_class_name=prefix_value",
|
||||
help =
|
||||
"""
|
||||
Turn the specified Pkl classes into Java classes with the `@ConfigurationProperties`
|
||||
annotation.
|
||||
|
||||
If this setting is omitted, the `@ConfigurationProperties` annotation is inferred
|
||||
according to the structure of the input modules.
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
.associate()
|
||||
.validate { map ->
|
||||
for (prefix in map.values) {
|
||||
require(prefix.isEmpty() || prefix.isValidConfigurationPropertiesPrefix) {
|
||||
"value must be a valid spring boot config prefix, but got '$prefix'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val helpString: String = "Generate Java classes and interfaces from Pkl module(s)"
|
||||
|
||||
override fun run() {
|
||||
@@ -147,6 +171,7 @@ class PklJavaCodegenCommand : ModulesCommand(name = "pkl-codegen-java", helpLink
|
||||
nonNullAnnotation = nonNullAnnotation,
|
||||
implementSerializable = implementSerializable,
|
||||
renames = renames,
|
||||
springBootConfigurationProperties = springBootConfigurationProperties,
|
||||
)
|
||||
CliJavaCodeGenerator(options).run()
|
||||
}
|
||||
|
||||
@@ -1651,6 +1651,65 @@ class JavaCodeGeneratorTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `spring boot -- explicitly configured ConfigurationProperties`() {
|
||||
val javaCode =
|
||||
generateJavaCode(
|
||||
"""
|
||||
module my.mod
|
||||
|
||||
class Server
|
||||
"""
|
||||
.trimIndent(),
|
||||
JavaCodeGeneratorOptions(
|
||||
generateSpringBootConfig = true,
|
||||
springBootConfigurationProperties =
|
||||
mapOf("my.mod" to "", "my.mod#Server" to "my-server-prefix"),
|
||||
),
|
||||
)
|
||||
assertThat(javaCode)
|
||||
.contains(
|
||||
"""
|
||||
|@ConfigurationProperties
|
||||
|public final class Mod {
|
||||
| private Mod() {
|
||||
| }
|
||||
"""
|
||||
.trimMargin()
|
||||
)
|
||||
assertThat(javaCode)
|
||||
.contains(
|
||||
"""
|
||||
| @ConfigurationProperties("my-server-prefix")
|
||||
| public static final class Server {
|
||||
"""
|
||||
.trimMargin()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `spring boot -- explicity configured properties means no interface`() {
|
||||
val javaCode =
|
||||
generateJavaCode(
|
||||
"""
|
||||
module my.mod
|
||||
|
||||
server: Server
|
||||
|
||||
class Server
|
||||
"""
|
||||
.trimIndent(),
|
||||
JavaCodeGeneratorOptions(
|
||||
generateSpringBootConfig = true,
|
||||
springBootConfigurationProperties = mapOf("foo" to "foo"),
|
||||
),
|
||||
)
|
||||
// normally this would generate `@ConfigurationProperties` annotations through inference.
|
||||
// but setting this option means that the user is in total control of how these annotations
|
||||
// get produced.
|
||||
assertThat(javaCode).doesNotContain("@ConfigurationProperties")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `import module`() {
|
||||
val library =
|
||||
|
||||
+11
-1
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -49,6 +49,15 @@ data class CliKotlinCodeGeneratorOptions(
|
||||
* original Pkl module name, and the value is the desired replacement.
|
||||
*/
|
||||
val renames: Map<String, String> = emptyMap(),
|
||||
|
||||
/**
|
||||
* The mapping of Pkl class names to the spring boot `@ConfigurationProperties` prefix name, when
|
||||
* [generateSpringBootConfig] is enabled.
|
||||
*
|
||||
* Setting this field is optional, and by default is inferred through the schema of the input
|
||||
* modules.
|
||||
*/
|
||||
val springBootConfigurationProperties: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("deprecated without replacement")
|
||||
@@ -62,5 +71,6 @@ data class CliKotlinCodeGeneratorOptions(
|
||||
implementSerializable,
|
||||
addGeneratedAnnotation,
|
||||
renames,
|
||||
springBootConfigurationProperties,
|
||||
)
|
||||
}
|
||||
@@ -53,7 +53,25 @@ data class KotlinCodeGeneratorOptions(
|
||||
* from the corresponding name derived from the Pkl module declaration .
|
||||
*/
|
||||
val renames: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Explicitly configures `@ConfigurationProperties` on generated classes.
|
||||
*
|
||||
* The key is the Pkl class name (e.g. `my.mod` for a module class, `my.mod#Server` for a nested
|
||||
* class), and the value is the prefix. An empty prefix generates a bare annotation.
|
||||
*
|
||||
* If non-empty, no `@ConfigurationProperties` annotations are inferred.
|
||||
*/
|
||||
val springBootConfigurationProperties: Map<String, String> = emptyMap(),
|
||||
) {
|
||||
init {
|
||||
for (value in springBootConfigurationProperties.values) {
|
||||
require(value.isEmpty() || value.isValidConfigurationPropertiesPrefix) {
|
||||
"Invalid value in `springBootConfigurationProperties`: expected a valid ConfigurationProperties prefix, but got '$value'."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KotlinCodeGeneratorException(message: String) : RuntimeException(message)
|
||||
|
||||
@@ -136,7 +154,11 @@ class KotlinCodeGenerator(
|
||||
|
||||
val hasModuleProperties = pModuleClass.properties.any { !it.value.isHidden }
|
||||
val isGenerateModuleClass =
|
||||
hasModuleProperties || pModuleClass.isOpen || pModuleClass.isAbstract
|
||||
hasModuleProperties ||
|
||||
pModuleClass.isOpen ||
|
||||
pModuleClass.isAbstract ||
|
||||
options.generateSpringBootConfig &&
|
||||
options.springBootConfigurationProperties.containsKey(pModuleClass.displayName)
|
||||
|
||||
fun generateCompanionRelatedCode(
|
||||
builder: TypeSpec.Builder,
|
||||
@@ -420,7 +442,21 @@ class KotlinCodeGenerator(
|
||||
}
|
||||
|
||||
fun generateSpringBootAnnotations(builder: TypeSpec.Builder) {
|
||||
if (isModuleClass) {
|
||||
if (options.springBootConfigurationProperties.isNotEmpty()) {
|
||||
// explicit configuration: the user is in total control, so don't infer anything
|
||||
val prefix = options.springBootConfigurationProperties[pClass.displayName] ?: return
|
||||
val annotation =
|
||||
AnnotationSpec.builder(
|
||||
ClassName("org.springframework.boot.context.properties", "ConfigurationProperties")
|
||||
)
|
||||
.apply {
|
||||
if (prefix.isNotEmpty()) {
|
||||
addMember("%S", prefix)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
builder.addAnnotation(annotation)
|
||||
} else if (isModuleClass) {
|
||||
builder.addAnnotation(
|
||||
ClassName("org.springframework.boot.context.properties", "ConfigurationProperties")
|
||||
)
|
||||
|
||||
@@ -22,12 +22,14 @@ import com.github.ajalt.clikt.parameters.options.associate
|
||||
import com.github.ajalt.clikt.parameters.options.default
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.github.ajalt.clikt.parameters.options.validate
|
||||
import com.github.ajalt.clikt.parameters.types.path
|
||||
import java.nio.file.Path
|
||||
import org.pkl.commons.cli.CliBaseOptions
|
||||
import org.pkl.commons.cli.cliMain
|
||||
import org.pkl.commons.cli.commands.ModulesCommand
|
||||
import org.pkl.commons.cli.commands.installCommonOptions
|
||||
import org.pkl.commons.isValidConfigurationPropertiesPrefix
|
||||
import org.pkl.commons.toPath
|
||||
import org.pkl.core.Release
|
||||
|
||||
@@ -72,6 +74,29 @@ class PklKotlinCodegenCommand : ModulesCommand(name = "pkl-codegen-kotlin", help
|
||||
)
|
||||
.flag()
|
||||
|
||||
private val springBootConfigurationProperties: Map<String, String> by
|
||||
option(
|
||||
names = arrayOf("--spring-boot-configuration-properties"),
|
||||
metavar = "pkl_class_name=prefix_value",
|
||||
help =
|
||||
"""
|
||||
Turn the specified Pkl classes into Kotlin classes with the `@ConfigurationProperties`
|
||||
annotation.
|
||||
|
||||
If this setting is omitted, the `@ConfigurationProperties` annotation is inferred
|
||||
according to the structure of the input modules.
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
.associate()
|
||||
.validate { map ->
|
||||
for (prefix in map.values) {
|
||||
require(prefix.isEmpty() || prefix.isValidConfigurationPropertiesPrefix) {
|
||||
"value must be a valid spring boot config prefix, but got '$prefix'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val implementSerializable: Boolean by
|
||||
option(
|
||||
names = arrayOf("--implement-serializable"),
|
||||
@@ -113,6 +138,7 @@ class PklKotlinCodegenCommand : ModulesCommand(name = "pkl-codegen-kotlin", help
|
||||
implementSerializable = implementSerializable,
|
||||
addGeneratedAnnotation = addGeneratedAnnotation,
|
||||
renames = renames,
|
||||
springBootConfigurationProperties = springBootConfigurationProperties,
|
||||
)
|
||||
CliKotlinCodeGenerator(options).run()
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ class KotlinCodeGeneratorTest {
|
||||
generateKdoc: Boolean = false,
|
||||
generateSpringBootConfig: Boolean = false,
|
||||
implementSerializable: Boolean = false,
|
||||
springBootConfigurationProperties: Map<String, String> = emptyMap(),
|
||||
): KotlinSourceCode {
|
||||
|
||||
val module = Evaluator.preconfigured().evaluateSchema(ModuleSource.text(pklCode))
|
||||
@@ -153,6 +154,7 @@ class KotlinCodeGeneratorTest {
|
||||
generateKdoc = generateKdoc,
|
||||
generateSpringBootConfig = generateSpringBootConfig,
|
||||
implementSerializable = implementSerializable,
|
||||
springBootConfigurationProperties = springBootConfigurationProperties,
|
||||
),
|
||||
)
|
||||
return KotlinSourceCode(generator.kotlinFile)
|
||||
@@ -1566,6 +1568,42 @@ class KotlinCodeGeneratorTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `spring boot -- explicitly configured ConfigurationProperties`() {
|
||||
val kotlinCode =
|
||||
generateKotlinCode(
|
||||
"""
|
||||
module my.mod
|
||||
|
||||
class Server
|
||||
"""
|
||||
.trimIndent(),
|
||||
generateSpringBootConfig = true,
|
||||
springBootConfigurationProperties =
|
||||
mapOf("my.mod" to "", "my.mod#Server" to "my-server-prefix"),
|
||||
)
|
||||
assertThat(kotlinCode).contains("@ConfigurationProperties\ndata class Mod")
|
||||
assertThat(kotlinCode).contains("@ConfigurationProperties(\"my-server-prefix\")")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `spring boot -- explicitly configured properties disables inference`() {
|
||||
val kotlinCode =
|
||||
generateKotlinCode(
|
||||
"""
|
||||
module my.mod
|
||||
|
||||
server: Server
|
||||
|
||||
class Server
|
||||
"""
|
||||
.trimIndent(),
|
||||
generateSpringBootConfig = true,
|
||||
springBootConfigurationProperties = mapOf("foo" to "foo"),
|
||||
)
|
||||
assertThat(kotlinCode).doesNotContain("@ConfigurationProperties")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `import module`() {
|
||||
val library =
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.pkl.gradle;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -367,6 +368,8 @@ public class PklPlugin implements Plugin<Project> {
|
||||
|
||||
spec.getGenerateSpringBootConfig().convention(false);
|
||||
|
||||
spec.getSpringBootConfigurationProperties().convention(Collections.emptyMap());
|
||||
|
||||
spec.getImplementSerializable().convention(false);
|
||||
|
||||
spec.getAddGeneratedAnnotation().convention(false);
|
||||
@@ -480,6 +483,7 @@ public class PklPlugin implements Plugin<Project> {
|
||||
task.getIndent().set(spec.getIndent());
|
||||
task.getOutputDir().set(spec.getOutputDir());
|
||||
task.getGenerateSpringBootConfig().set(spec.getGenerateSpringBootConfig());
|
||||
task.getSpringBootConfigurationProperties().set(spec.getSpringBootConfigurationProperties());
|
||||
task.getImplementSerializable().set(spec.getImplementSerializable());
|
||||
task.getAddGeneratedAnnotation().set(spec.getAddGeneratedAnnotation());
|
||||
task.getRenames().set(spec.getRenames());
|
||||
|
||||
@@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,6 +32,8 @@ public interface CodeGenSpec extends ModulesSpec {
|
||||
|
||||
Property<Boolean> getGenerateSpringBootConfig();
|
||||
|
||||
MapProperty<String, String> getSpringBootConfigurationProperties();
|
||||
|
||||
Property<Boolean> getImplementSerializable();
|
||||
|
||||
MapProperty<String, String> getRenames();
|
||||
|
||||
@@ -38,6 +38,9 @@ public abstract class CodeGenTask extends ModulesTask {
|
||||
@Input
|
||||
public abstract Property<Boolean> getGenerateSpringBootConfig();
|
||||
|
||||
@Input
|
||||
public abstract MapProperty<String, String> getSpringBootConfigurationProperties();
|
||||
|
||||
@Input
|
||||
public abstract Property<Boolean> getImplementSerializable();
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ public abstract class JavaCodeGenTask extends CodeGenTask {
|
||||
getParamsAnnotation().getOrNull(),
|
||||
getNonNullAnnotation().getOrNull(),
|
||||
getImplementSerializable().get(),
|
||||
getRenames().get()))
|
||||
getRenames().get(),
|
||||
getSpringBootConfigurationProperties().get()))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,8 @@ public abstract class KotlinCodeGenTask extends CodeGenTask {
|
||||
getGenerateSpringBootConfig().get(),
|
||||
getImplementSerializable().get(),
|
||||
getAddGeneratedAnnotation().get(),
|
||||
getRenames().get()))
|
||||
getRenames().get(),
|
||||
getSpringBootConfigurationProperties().get()))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user