Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version = "3.10.0"
runner.dialect = scala3

maxColumn = 100

docstrings.style = Asterisk
docstrings.blankFirstLine = yes
docstrings.wrap = yes

rewrite.scala3.removeOptionalBraces = false
rewrite.insertBraces.minLines = 1 // Or 2
rewrite.insertBraces.allBlocks = true
15 changes: 14 additions & 1 deletion build-logic/src/main/kotlin/pklAllProjects.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ val buildInfo = extensions.create<BuildInfo>("buildInfo", project)

configurations {
val rejectedVersionSuffix = Regex("-alpha|-beta|-eap|-m|-rc|-snapshot", RegexOption.IGNORE_CASE)
val versionSuffixRejectionExemptions =
setOf(
// I know.
// This looks odd.
// But yes, it's transitively required by one of the release versions of `zinc`
// https://github.com/sbt/zinc/blame/57a2df7104b3ce27b46404bb09a0126bd4013427/project/Dependencies.scala#L85
"com.eed3si9n:shaded-scalajson_2.13:1.0.0-M4"
)
configureEach {
resolutionStrategy {
// forbid dependencies whose pom.xml's include version ranges, because this will lead to
Expand All @@ -30,7 +38,12 @@ configurations {
failOnDynamicVersions()
componentSelection {
all {
if (rejectedVersionSuffix.containsMatchIn(candidate.version)) {
if (
rejectedVersionSuffix.containsMatchIn(candidate.version) &&
!versionSuffixRejectionExemptions.contains(
"${candidate.group}:${candidate.module}:${candidate.version}"
)
) {
reject(
"Rejected dependency $candidate " +
"because it has a prelease version suffix matching `$rejectedVersionSuffix`."
Expand Down
61 changes: 61 additions & 0 deletions build-logic/src/main/kotlin/pklScalaLibrary.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.
* 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.
*/
@file:Suppress("HttpUrlsUsage", "unused")

import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.kotlin.dsl.withType

plugins {
id("pklJavaLibrary")
scala
}

// Build configuration.
val buildInfo = project.extensions.getByType<BuildInfo>()

// Version Catalog library symbols.
val libs = the<LibrariesForLibs>()

Comment thread
bioball marked this conversation as resolved.
dependencies {
testImplementation(libs.scalaTestPlusJunit)
testImplementation(libs.scalaTest)
testImplementation(libs.diffx)
}

scala { scalaVersion = libs.versions.scala }

tasks.withType<ScalaCompile>().configureEach {
scalaCompileOptions.additionalParameters =
listOf("-Xsource:3", "-release:${buildInfo.jvmTarget}", "-target:${buildInfo.jvmTarget}")
}

tasks.test {
useJUnitPlatform {
includeEngines("scalatest")
testLogging { events("passed", "skipped", "failed") }
}
}

spotless {
scala {
scalafmt(libs.versions.scalafmt.get()).configFile(rootProject.file(".scalafmt.conf"))
target("src/*/scala/**/*.scala")
licenseHeaderFile(
rootProject.file("build-logic/src/main/resources/license-header.star-block.txt"),
"package ",
)
}
}
10 changes: 10 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ checksumPlugin = "1.4.0"
# 5.0.3 is the last version compatible with Kotlin 2.2
clikt = "5.0.3"
commonMark = "0.28.0"
diffx = "0.9.0"
downloadTaskPlugin = "5.7.0"
errorProne = "2.49.0"
errorPronePlugin = "5.1.0"
Expand Down Expand Up @@ -59,6 +60,10 @@ nullaway = "0.13.4"
nullawayPlugin = "3.0.0"
nuValidator = "26.4.16"
paguro = "3.10.3"
scala = "2.13.17"
scalafmt = "3.10.0"
scalaTest = "3.2.19"
scalaTestPlusJunit = "3.2.19.0"
shadowPlugin = "9.4.1"
slf4j = "2.0.17"
snakeYaml = "3.0.1"
Expand All @@ -71,6 +76,7 @@ clikt = { group = "com.github.ajalt.clikt", name = "clikt", version.ref = "clikt
cliktMarkdown = { group = "com.github.ajalt.clikt", name = "clikt-markdown", version.ref = "clikt" }
commonMark = { group = "org.commonmark", name = "commonmark", version.ref = "commonMark" }
commonMarkTables = { group = "org.commonmark", name = "commonmark-ext-gfm-tables", version.ref = "commonMark" }
diffx = { group = "com.softwaremill.diffx", name = "diffx-scalatest-should_2.13", version.ref = "diffx" }
downloadTaskPlugin = { group = "de.undercouch", name = "gradle-download-task", version.ref = "downloadTaskPlugin" }
#noinspection UnusedVersionCatalogEntry
errorProne = { group = "com.google.errorprone", name = "error_prone_core", version.ref = "errorProne" }
Expand Down Expand Up @@ -113,6 +119,10 @@ nullawayPlugin = { group = "net.ltgt.gradle", name = "gradle-nullaway-plugin", v
# to be replaced with https://github.com/usethesource/capsule or https://github.com/lacuna/bifurcan
paguro = { group = "org.organicdesign", name = "Paguro", version.ref = "paguro" }
pklConfigJavaAll025 = { group = "org.pkl-lang", name = "pkl-config-java-all", version = "0.25.0" }
scalaLibrary = { group = "org.scala-lang", name = "scala-library", version.ref = "scala" }
scalaReflect = { group = "org.scala-lang", name = "scala-reflect", version.ref = "scala" }
scalaTest = { group = "org.scalatest", name = "scalatest_2.13", version.ref = "scalaTest" }
scalaTestPlusJunit = { group = "org.scalatestplus", name = "junit-5-12_2.13", version.ref = "scalaTestPlusJunit" }
shadowPlugin = { group = "com.gradleup.shadow", name = "com.gradleup.shadow.gradle.plugin", version.ref = "shadowPlugin" }
slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
slf4jSimple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
Expand Down
36 changes: 36 additions & 0 deletions pkl-config-scala/pkl-config-scala.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.
* 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.
*/
plugins {
id("pklAllProjects")
id("pklScalaLibrary")
id("pklPublishLibrary")
}

dependencies {
implementation(projects.pklConfigJava)
api(libs.scalaReflect)
}

publishing {
publications {
named<MavenPublication>("library") {
pom {
url.set("https://github.com/apple/pkl/tree/main/pkl-config-scala")
description.set("Scala config library based on the Pkl config language.")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright © 2025-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.scala.mapper

import org.pkl.config.java.mapper.{Converter, ConverterFactory, Reflection, ValueMapper}
import org.pkl.config.scala.mapper.JavaReflectionSyntaxExtensions.*
import org.pkl.core.PClassInfo

import java.lang.reflect.Type
import java.util.Optional
import scala.jdk.OptionConverters.RichOption
import scala.reflect.ClassTag

/**
* Provides infrastructure that helps define custom converter factories in a somewhat concise way at
* the same time utilizing caching.
*/
private[mapper] object CachedConverterFactories {

/**
* Function used in converters that essentially does a conversion logic.
*
* @tparam S
* source type
* @tparam C
* cache. represented by `CachedSourceTypeInfo` for single-param generic types and
* `(CachedSourceTypeInfo, CachedSourceTypeInfo)` for two-param types.
* @tparam T
* target type
*/
private type ConversionFunction[S, C, T] = (S, C, ValueMapper) => T

/**
* A converter for single-parameter types, caching conversion functions.
*
* @param conv
* A function that defines the conversion logic using the cached `CachedSourceTypeInfo`.
*/
private final class Converter1[S, T](
conv: ConversionFunction[S, CachedSourceTypeInfo, T]
) extends Converter[S, T] {
private val s1 = new CachedSourceTypeInfo()
override def convert(value: S, valueMapper: ValueMapper): T = {
conv.apply(value, s1, valueMapper)
}
}

/**
* A converter for two-parameter types (e.g., Tuple2 or Map), caching conversion functions.
*
* @param conv
* A function that defines the conversion logic using two instances of `CachedSourceTypeInfo`.
*/
private final class Converter2[S, T](
conv: ConversionFunction[
S,
(CachedSourceTypeInfo, CachedSourceTypeInfo),
T
]
) extends Converter[S, T] {
private val s1 = new CachedSourceTypeInfo()
private val s2 = new CachedSourceTypeInfo()
override def convert(value: S, valueMapper: ValueMapper): T = {
conv.apply(value, (s1, s2), valueMapper)
}
}

/**
* A factory for creating converters based on parameterized types, supporting generic conversion.
*
* @param acceptSourceType
* Predicate to determine if the source type is acceptable.
* @param extractTypeParams
* Function to extract type parameters from the `ParameterizedType`.
* @param newConverter
* Function to create a new converter based on extracted type parameters.
*/
private final class ParametrizinglyTypedConverterFactory[T: ClassTag, TT](
acceptSourceType: PClassInfo[?] => Boolean,
extractTypeParams: Type => Option[TT],
newConverter: TT => Converter[?, ?]
) extends ConverterFactory {
private val targetClassTag: ClassTag[T] = implicitly

override def create(
sourceType: PClassInfo[?],
targetType: Type
): Optional[Converter[?, ?]] = {
if (acceptSourceType(sourceType)) {
val targetClass = Reflection.toRawType(targetType)
if (targetClassTag.runtimeClass.isAssignableFrom(targetClass)) {
val typeParams = extractTypeParams(
Reflection.getExactSupertype(targetType, targetClass)
)
typeParams.map(newConverter).toJava
} else {
Optional.empty()
}
} else {
Optional.empty()
}
}
}

/**
* Factory method for single-parameter types such as `List` or `Option`, using cached conversion.
*
* @param acceptSourceType
* Predicate to determine if the source type is acceptable.
* @param conv
* Conversion function applied to the value and cache.
*/
def forParametrizedType1[S, T: ClassTag](
acceptSourceType: PClassInfo[?] => Boolean,
conv: Type => ConversionFunction[
S,
CachedSourceTypeInfo,
T
]
): ConverterFactory = new ParametrizinglyTypedConverterFactory[T, Type](
acceptSourceType,
_.params1,
t1 => new Converter1(conv(t1))
)

/**
* Factory method for two-parameter types such as `Map` or `Tuple2`, using cached conversion.
*
* @param acceptSourceType
* Predicate to determine if the source type is acceptable.
* @param conv
* Conversion function applied to the value and cache.
*/
def forParametrizedType2[S, T: ClassTag](
acceptSourceType: PClassInfo[?] => Boolean,
conv: (Type, Type) => ConversionFunction[
S,
(CachedSourceTypeInfo, CachedSourceTypeInfo),
T
]
): ConverterFactory = {
new ParametrizinglyTypedConverterFactory[T, (Type, Type)](
acceptSourceType,
_.params2,
{ case (t1, t2) => new Converter2(conv(t1, t2)) }
)
}
}
Loading
Loading