pkl-config-java: Replace Config.fromPklBinary() with ConfigDecoder (#1533)

Motivation:
- `Config` mixes configuration representation with decoding logic
- `Config.fromPklBinary()` does not scale as decoding gains options
(e.g., binary versions or formats)
- The decoding API is inconsistent with `ConfigEvaluator`

Changes:
- Introduce `ConfigDecoder` (with builder) and move
`Config.fromPklBinary()` logic into it
- Deprecate `Config.fromPklBinary()` methods for removal
- Add `ConfigDecoder.forKotlin()` extension function
- Update and improve tests

Result:
- Decoding is separated from `Config` and exposed via a dedicated API
- Decoding can evolve independently (e.g., adding options such as binary
versions or supporting new formats)
- Evaluation and decoding APIs follow a consistent design
This commit is contained in:
odenix
2026-04-20 19:09:42 +01:00
committed by GitHub
parent 07c68239b9
commit 7a75ab57f5
18 changed files with 638 additions and 179 deletions
@@ -18,6 +18,8 @@ package org.pkl.config.kotlin
import kotlin.reflect.jvm.javaType
import kotlin.reflect.typeOf
import org.pkl.config.java.Config
import org.pkl.config.java.ConfigDecoder
import org.pkl.config.java.ConfigDecoderBuilder
import org.pkl.config.java.ConfigEvaluator
import org.pkl.config.java.ConfigEvaluatorBuilder
import org.pkl.config.java.mapper.ConversionException
@@ -61,5 +63,20 @@ fun ValueMapperBuilder.forKotlin(): ValueMapperBuilder =
fun ConfigEvaluatorBuilder.forKotlin(): ConfigEvaluatorBuilder =
setValueMapperBuilder(valueMapperBuilder.forKotlin())
/**
* Returns a new [ConfigEvaluator] with added conversions and converter factories for Kotlin types.
*/
fun ConfigEvaluator.forKotlin(): ConfigEvaluator =
setValueMapper(valueMapper.toBuilder().forKotlin().build())
/**
* Configures this [ConfigDecoderBuilder] with conversions and converter factories for Kotlin types.
*/
fun ConfigDecoderBuilder.forKotlin(): ConfigDecoderBuilder =
setValueMapperBuilder(valueMapperBuilder.forKotlin())
/**
* Returns a new [ConfigDecoder] with added conversions and converter factories for Kotlin types.
*/
fun ConfigDecoder.forKotlin(): ConfigDecoder =
setValueMapper(valueMapper.toBuilder().forKotlin().build())
@@ -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.
@@ -18,32 +18,30 @@ package org.pkl.config.kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.pkl.config.java.ConfigEvaluator
import org.pkl.config.java.ConfigEvaluatorBuilder
import org.pkl.config.java.Config
import org.pkl.config.java.mapper.ConversionException
import org.pkl.config.kotlin.ConfigExtensionsTest.Hobby.READING
import org.pkl.config.kotlin.ConfigExtensionsTest.Hobby.SWIMMING
import org.pkl.core.ModuleSource.text
import org.pkl.config.kotlin.AbstractConfigExtensionsTest.Hobby.READING
import org.pkl.config.kotlin.AbstractConfigExtensionsTest.Hobby.SWIMMING
class ConfigExtensionsTest {
private val evaluator = ConfigEvaluator.preconfigured().forKotlin()
abstract class AbstractConfigExtensionsTest {
protected abstract fun loadConfig(text: String): Config
@Test
fun `convert to kotlin classes`() {
val config =
evaluator.evaluate(
text(
"""
pigeon {
name = "pigeon"
age = 30
hobbies = List("swimming", "reading")
address {
street = "Fuzzy St."
}
}
loadConfig(
"""
)
pigeon {
name = "pigeon"
age = 30
hobbies = List("swimming", "reading")
address {
street = "Fuzzy St."
}
}
"""
.trimIndent()
)
val address = config["pigeon"]["address"].to<Address<String>>()
@@ -60,9 +58,7 @@ class ConfigExtensionsTest {
@Test
fun `convert to kotlin class with nullable property`() {
// cover ConfigEvaluatorBuilder.preconfigured()
val evaluator = ConfigEvaluatorBuilder.preconfigured().forKotlin().build()
val config = evaluator.evaluate(text("pigeon { address = null }"))
val config = loadConfig("pigeon { address = null }")
val pigeon = config["pigeon"].to<Person2>()
assertThat(pigeon.address).isNull()
@@ -71,10 +67,8 @@ class ConfigExtensionsTest {
@Test
fun `convert to kotlin class with covariant collection property type`() {
val config =
evaluator.evaluate(
text(
"""pigeon { addresses = List(new Dynamic { street = "Fuzzy St." }, new Dynamic { street = "Other St." }) }"""
)
loadConfig(
"""pigeon { addresses = List(new Dynamic { street = "Fuzzy St." }, new Dynamic { street = "Other St." }) }"""
)
config["pigeon"].to<Person3>()
@@ -82,8 +76,7 @@ class ConfigExtensionsTest {
@Test
fun `convert to nullable type`() {
val config =
evaluator.evaluate(text("""pigeon { address1 { street = "Fuzzy St." }; address2 = null }"""))
val config = loadConfig("""pigeon { address1 { street = "Fuzzy St." }; address2 = null }""")
val address1 = config["pigeon"]["address1"].to<Address<String>?>()
assertThat(address1).isEqualTo(Address(street = "Fuzzy St."))
@@ -102,16 +95,14 @@ class ConfigExtensionsTest {
@Test
fun `convert to kotlin class that has defaults for constructor args`() {
val config =
evaluator.evaluate(
text(
"""
loadConfig(
"""
pigeon {
name = "Pigeon"
age = 42
hobbies = List()
}
"""
)
)
val pigeon = config["pigeon"].to<PersonWithDefaults>()
@@ -124,16 +115,14 @@ class ConfigExtensionsTest {
@Test
fun `convert to java class with multiple constructors`() {
val config =
evaluator.evaluate(
text(
"""
loadConfig(
"""
pigeon {
name = "Pigeon"
age = 42
hobbies = List()
}
"""
)
)
val pigeon = config["pigeon"].to<JavaPerson>()
@@ -145,10 +134,8 @@ class ConfigExtensionsTest {
@Test
fun `convert list to parameterized list`() {
val config =
evaluator.evaluate(
text(
"""friends = List(new Dynamic { name = "lilly"}, new Dynamic {name = "bob"}, new Dynamic {name = "susan"})"""
)
loadConfig(
"""friends = List(new Dynamic { name = "lilly"}, new Dynamic {name = "bob"}, new Dynamic {name = "susan"})"""
)
val friends = config["friends"].to<List<SimplePerson>>()
@@ -159,10 +146,8 @@ class ConfigExtensionsTest {
@Test
fun `convert map to parameterized map`() {
val config =
evaluator.evaluate(
text(
"""friends = Map("l", new Dynamic { name = "lilly"}, "b", new Dynamic { name = "bob"}, "s", new Dynamic { name = "susan"})"""
)
loadConfig(
"""friends = Map("l", new Dynamic { name = "lilly"}, "b", new Dynamic { name = "bob"}, "s", new Dynamic { name = "susan"})"""
)
val friends = config["friends"].to<Map<String, SimplePerson>>()
@@ -179,9 +164,7 @@ class ConfigExtensionsTest {
@Test
fun `convert container to parameterized map`() {
val config =
evaluator.evaluate(
text("""friends {l { name = "lilly"}; b { name = "bob"}; s { name = "susan"}}""")
)
loadConfig("""friends {l { name = "lilly"}; b { name = "bob"}; s { name = "susan"}}""")
val friends = config["friends"].to<Map<String, SimplePerson>>()
assertThat(friends)
@@ -198,14 +181,12 @@ class ConfigExtensionsTest {
fun `convert enum with mangled names`() {
val values = MangledNameEnum.entries.map { "\"$it\"" }
val config =
evaluator.evaluate(
text(
"""
loadConfig(
"""
typealias MangledNameEnum = ${values.joinToString(" | ")}
allEnumValues: Set<MangledNameEnum> = Set(${values.joinToString(", ")})
"""
.trimIndent()
)
.trimIndent()
)
val allEnumValues = config["allEnumValues"].to<Set<MangledNameEnum>>()
assertThat(allEnumValues).isEqualTo(MangledNameEnum.entries.toSet())
@@ -0,0 +1,62 @@
/*
* 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.config.kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Test
import org.pkl.config.java.Config
import org.pkl.config.java.ConfigDecoder
import org.pkl.config.java.ConfigDecoderBuilder
import org.pkl.config.java.mapper.ValueMapperBuilder
import org.pkl.core.Evaluator
import org.pkl.core.ModuleSource
class ConfigDecoderExtensionsTest : AbstractConfigExtensionsTest() {
companion object {
private val evaluator = Evaluator.preconfigured()
@AfterAll
@JvmStatic
fun afterAll() {
evaluator.close()
}
}
override fun loadConfig(text: String): Config {
val updatedText =
"""
import "pkl:pklbinary"
$text
output {
renderer = new pklbinary.Renderer {}
}
"""
.trimIndent()
val bytes = evaluator.evaluateOutputBytes(ModuleSource.text(updatedText))
return ConfigDecoder.preconfigured().forKotlin().decode(bytes)
}
@Test
fun `ConfigDecoderBuilder_forKotlin configures its valueMapperBuilder accordingly`() {
val builderMapper = ConfigDecoderBuilder.preconfigured().forKotlin().valueMapperBuilder
val referenceMapper = ValueMapperBuilder.preconfigured().forKotlin()
assertThat(builderMapper.conversions.size).isEqualTo(referenceMapper.conversions.size)
assertThat(builderMapper.converterFactories.size)
.isEqualTo(referenceMapper.converterFactories.size)
}
}
@@ -0,0 +1,49 @@
/*
* 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.config.kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Test
import org.pkl.config.java.Config
import org.pkl.config.java.ConfigEvaluator
import org.pkl.config.java.ConfigEvaluatorBuilder
import org.pkl.config.java.mapper.ValueMapperBuilder
import org.pkl.core.ModuleSource.text
class ConfigEvaluatorExtensionsTest : AbstractConfigExtensionsTest() {
companion object {
private val evaluator = ConfigEvaluator.preconfigured().forKotlin()
@AfterAll
@JvmStatic
fun afterAll() {
evaluator.close()
}
}
override fun loadConfig(text: String): Config = evaluator.evaluate(text(text))
@Test
fun `ConfigEvaluaterBuilder_forKotlin configures its valueMapperBuilder accordingly`() {
val builderMapper = ConfigEvaluatorBuilder.preconfigured().forKotlin().valueMapperBuilder
val referenceMapper = ValueMapperBuilder.preconfigured().forKotlin()
assertThat(builderMapper.conversions.size).isEqualTo(referenceMapper.conversions.size)
assertThat(builderMapper.converterFactories.size)
.isEqualTo(referenceMapper.converterFactories.size)
}
}