mirror of
https://github.com/apple/pkl.git
synced 2026-05-25 16:19:20 +02:00
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:
@@ -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())
|
||||
|
||||
+34
-53
@@ -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())
|
||||
+62
@@ -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)
|
||||
}
|
||||
}
|
||||
+49
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user