Skip to content

Commit c8a7a01

Browse files
author
Andriy Onyshchuk
committed
PKL Config Scala
1 parent 9c1a9cb commit c8a7a01

18 files changed

Lines changed: 1503 additions & 1 deletion

.scalafmt.conf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version = "3.10.0"
2+
runner.dialect = scala3
3+
4+
maxColumn = 100
5+
6+
docstrings.style = Asterisk
7+
docstrings.blankFirstLine = yes
8+
docstrings.wrap = yes
9+
10+
rewrite.scala3.removeOptionalBraces = false
11+
rewrite.insertBraces.minLines = 1 // Or 2
12+
rewrite.insertBraces.allBlocks = true

build-logic/src/main/kotlin/pklAllProjects.gradle.kts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ val buildInfo = extensions.create<BuildInfo>("buildInfo", project)
2222

2323
configurations {
2424
val rejectedVersionSuffix = Regex("-alpha|-beta|-eap|-m|-rc|-snapshot", RegexOption.IGNORE_CASE)
25+
val versionSuffixRejectionExemptions =
26+
setOf(
27+
// I know.
28+
// This looks odd.
29+
// But yes, it's transitively required by one of the release versions of `zinc`
30+
// https://github.com/sbt/zinc/blame/57a2df7104b3ce27b46404bb09a0126bd4013427/project/Dependencies.scala#L85
31+
"com.eed3si9n:shaded-scalajson_2.13:1.0.0-M4"
32+
)
2533
configureEach {
2634
resolutionStrategy {
2735
// forbid dependencies whose pom.xml's include version ranges, because this will lead to
@@ -30,7 +38,12 @@ configurations {
3038
failOnDynamicVersions()
3139
componentSelection {
3240
all {
33-
if (rejectedVersionSuffix.containsMatchIn(candidate.version)) {
41+
if (
42+
rejectedVersionSuffix.containsMatchIn(candidate.version) &&
43+
!versionSuffixRejectionExemptions.contains(
44+
"${candidate.group}:${candidate.module}:${candidate.version}"
45+
)
46+
) {
3447
reject(
3548
"Rejected dependency $candidate " +
3649
"because it has a prelease version suffix matching `$rejectedVersionSuffix`."
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@file:Suppress("HttpUrlsUsage", "unused")
17+
18+
import org.gradle.accessors.dm.LibrariesForLibs
19+
import org.gradle.kotlin.dsl.withType
20+
21+
plugins {
22+
id("pklJavaLibrary")
23+
scala
24+
}
25+
26+
// Build configuration.
27+
val buildInfo = project.extensions.getByType<BuildInfo>()
28+
29+
// Version Catalog library symbols.
30+
val libs = the<LibrariesForLibs>()
31+
32+
dependencies {
33+
testImplementation(libs.scalaTestPlusJunit)
34+
testImplementation(libs.scalaTest)
35+
testImplementation(libs.diffx)
36+
}
37+
38+
scala { scalaVersion = libs.versions.scala }
39+
40+
tasks.withType<ScalaCompile>().configureEach {
41+
scalaCompileOptions.additionalParameters =
42+
listOf("-Xsource:3", "-release:${buildInfo.jvmTarget}", "-target:${buildInfo.jvmTarget}")
43+
}
44+
45+
tasks.test {
46+
useJUnitPlatform {
47+
includeEngines("scalatest")
48+
testLogging { events("passed", "skipped", "failed") }
49+
}
50+
}
51+
52+
spotless {
53+
scala {
54+
scalafmt(libs.versions.scalafmt.get()).configFile(rootProject.file(".scalafmt.conf"))
55+
target("src/*/scala/**/*.scala")
56+
licenseHeaderFile(
57+
rootProject.file("build-logic/src/main/resources/license-header.star-block.txt"),
58+
"package ",
59+
)
60+
}
61+
}

gradle/libs.versions.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ checksumPlugin = "1.4.0"
44
# 5.0.3 is the last version compatible with Kotlin 2.2
55
clikt = "5.0.3"
66
commonMark = "0.28.0"
7+
diffx = "0.9.0"
78
downloadTaskPlugin = "5.7.0"
89
errorProne = "2.49.0"
910
errorPronePlugin = "5.1.0"
@@ -59,6 +60,10 @@ nullaway = "0.13.4"
5960
nullawayPlugin = "3.0.0"
6061
nuValidator = "26.4.16"
6162
paguro = "3.10.3"
63+
scala = "2.13.17"
64+
scalafmt = "3.10.0"
65+
scalaTest = "3.2.19"
66+
scalaTestPlusJunit = "3.2.19.0"
6267
shadowPlugin = "9.4.1"
6368
slf4j = "2.0.17"
6469
snakeYaml = "3.0.1"
@@ -71,6 +76,7 @@ clikt = { group = "com.github.ajalt.clikt", name = "clikt", version.ref = "clikt
7176
cliktMarkdown = { group = "com.github.ajalt.clikt", name = "clikt-markdown", version.ref = "clikt" }
7277
commonMark = { group = "org.commonmark", name = "commonmark", version.ref = "commonMark" }
7378
commonMarkTables = { group = "org.commonmark", name = "commonmark-ext-gfm-tables", version.ref = "commonMark" }
79+
diffx = { group = "com.softwaremill.diffx", name = "diffx-scalatest-should_2.13", version.ref = "diffx" }
7480
downloadTaskPlugin = { group = "de.undercouch", name = "gradle-download-task", version.ref = "downloadTaskPlugin" }
7581
#noinspection UnusedVersionCatalogEntry
7682
errorProne = { group = "com.google.errorprone", name = "error_prone_core", version.ref = "errorProne" }
@@ -113,6 +119,10 @@ nullawayPlugin = { group = "net.ltgt.gradle", name = "gradle-nullaway-plugin", v
113119
# to be replaced with https://github.com/usethesource/capsule or https://github.com/lacuna/bifurcan
114120
paguro = { group = "org.organicdesign", name = "Paguro", version.ref = "paguro" }
115121
pklConfigJavaAll025 = { group = "org.pkl-lang", name = "pkl-config-java-all", version = "0.25.0" }
122+
scalaLibrary = { group = "org.scala-lang", name = "scala-library", version.ref = "scala" }
123+
scalaReflect = { group = "org.scala-lang", name = "scala-reflect", version.ref = "scala" }
124+
scalaTest = { group = "org.scalatest", name = "scalatest_2.13", version.ref = "scalaTest" }
125+
scalaTestPlusJunit = { group = "org.scalatestplus", name = "junit-5-12_2.13", version.ref = "scalaTestPlusJunit" }
116126
shadowPlugin = { group = "com.gradleup.shadow", name = "com.gradleup.shadow.gradle.plugin", version.ref = "shadowPlugin" }
117127
slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
118128
slf4jSimple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
plugins {
17+
id("pklAllProjects")
18+
id("pklScalaLibrary")
19+
id("pklPublishLibrary")
20+
}
21+
22+
dependencies {
23+
implementation(projects.pklConfigJava)
24+
api(libs.scalaReflect)
25+
}
26+
27+
publishing {
28+
publications {
29+
named<MavenPublication>("library") {
30+
pom {
31+
url.set("https://github.com/apple/pkl/tree/main/pkl-config-scala")
32+
description.set("Scala config library based on the Pkl config language.")
33+
}
34+
}
35+
}
36+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.pkl.config.scala.mapper
17+
18+
import org.pkl.config.java.mapper.{Converter, ConverterFactory, Reflection, ValueMapper}
19+
import org.pkl.config.scala.mapper.JavaReflectionSyntaxExtensions.*
20+
import org.pkl.core.PClassInfo
21+
22+
import java.lang.reflect.Type
23+
import java.util.Optional
24+
import scala.jdk.OptionConverters.RichOption
25+
import scala.reflect.ClassTag
26+
27+
/**
28+
* Provides infrastructure that helps define custom converter factories in a somewhat concise way at
29+
* the same time utilizing caching.
30+
*/
31+
private[mapper] object CachedConverterFactories {
32+
33+
/**
34+
* Function used in converters that essentially does a conversion logic.
35+
*
36+
* @tparam S
37+
* source type
38+
* @tparam C
39+
* cache. represented by `CachedSourceTypeInfo` for single-param generic types and
40+
* `(CachedSourceTypeInfo, CachedSourceTypeInfo)` for two-param types.
41+
* @tparam T
42+
* target type
43+
*/
44+
private type ConversionFunction[S, C, T] = (S, C, ValueMapper) => T
45+
46+
/**
47+
* A converter for single-parameter types, caching conversion functions.
48+
*
49+
* @param conv
50+
* A function that defines the conversion logic using the cached `CachedSourceTypeInfo`.
51+
*/
52+
private final class Converter1[S, T](
53+
conv: ConversionFunction[S, CachedSourceTypeInfo, T]
54+
) extends Converter[S, T] {
55+
private val s1 = new CachedSourceTypeInfo()
56+
override def convert(value: S, valueMapper: ValueMapper): T = {
57+
conv.apply(value, s1, valueMapper)
58+
}
59+
}
60+
61+
/**
62+
* A converter for two-parameter types (e.g., Tuple2 or Map), caching conversion functions.
63+
*
64+
* @param conv
65+
* A function that defines the conversion logic using two instances of `CachedSourceTypeInfo`.
66+
*/
67+
private final class Converter2[S, T](
68+
conv: ConversionFunction[
69+
S,
70+
(CachedSourceTypeInfo, CachedSourceTypeInfo),
71+
T
72+
]
73+
) extends Converter[S, T] {
74+
private val s1 = new CachedSourceTypeInfo()
75+
private val s2 = new CachedSourceTypeInfo()
76+
override def convert(value: S, valueMapper: ValueMapper): T = {
77+
conv.apply(value, (s1, s2), valueMapper)
78+
}
79+
}
80+
81+
/**
82+
* A factory for creating converters based on parameterized types, supporting generic conversion.
83+
*
84+
* @param acceptSourceType
85+
* Predicate to determine if the source type is acceptable.
86+
* @param extractTypeParams
87+
* Function to extract type parameters from the `ParameterizedType`.
88+
* @param newConverter
89+
* Function to create a new converter based on extracted type parameters.
90+
*/
91+
private final class ParametrizinglyTypedConverterFactory[T: ClassTag, TT](
92+
acceptSourceType: PClassInfo[?] => Boolean,
93+
extractTypeParams: Type => Option[TT],
94+
newConverter: TT => Converter[?, ?]
95+
) extends ConverterFactory {
96+
private val targetClassTag: ClassTag[T] = implicitly
97+
98+
override def create(
99+
sourceType: PClassInfo[?],
100+
targetType: Type
101+
): Optional[Converter[?, ?]] = {
102+
if (acceptSourceType(sourceType)) {
103+
val targetClass = Reflection.toRawType(targetType)
104+
if (targetClassTag.runtimeClass.isAssignableFrom(targetClass)) {
105+
val typeParams = extractTypeParams(
106+
Reflection.getExactSupertype(targetType, targetClass)
107+
)
108+
typeParams.map(newConverter).toJava
109+
} else {
110+
Optional.empty()
111+
}
112+
} else {
113+
Optional.empty()
114+
}
115+
}
116+
}
117+
118+
/**
119+
* Factory method for single-parameter types such as `List` or `Option`, using cached conversion.
120+
*
121+
* @param acceptSourceType
122+
* Predicate to determine if the source type is acceptable.
123+
* @param conv
124+
* Conversion function applied to the value and cache.
125+
*/
126+
def forParametrizedType1[S, T: ClassTag](
127+
acceptSourceType: PClassInfo[?] => Boolean,
128+
conv: Type => ConversionFunction[
129+
S,
130+
CachedSourceTypeInfo,
131+
T
132+
]
133+
): ConverterFactory = new ParametrizinglyTypedConverterFactory[T, Type](
134+
acceptSourceType,
135+
_.params1,
136+
t1 => new Converter1(conv(t1))
137+
)
138+
139+
/**
140+
* Factory method for two-parameter types such as `Map` or `Tuple2`, using cached conversion.
141+
*
142+
* @param acceptSourceType
143+
* Predicate to determine if the source type is acceptable.
144+
* @param conv
145+
* Conversion function applied to the value and cache.
146+
*/
147+
def forParametrizedType2[S, T: ClassTag](
148+
acceptSourceType: PClassInfo[?] => Boolean,
149+
conv: (Type, Type) => ConversionFunction[
150+
S,
151+
(CachedSourceTypeInfo, CachedSourceTypeInfo),
152+
T
153+
]
154+
): ConverterFactory = {
155+
new ParametrizinglyTypedConverterFactory[T, (Type, Type)](
156+
acceptSourceType,
157+
_.params2,
158+
{ case (t1, t2) => new Converter2(conv(t1, t2)) }
159+
)
160+
}
161+
}

0 commit comments

Comments
 (0)