From 95782adf3423fa342e320d11ad8b24b01b39f0ff Mon Sep 17 00:00:00 2001 From: Andriy Onyshchuk Date: Fri, 23 May 2025 00:49:30 -0700 Subject: [PATCH 1/7] PKL Config Scala --- .../src/main/kotlin/pklAllProjects.gradle.kts | 11 +- .../src/main/kotlin/pklJavaLibrary.gradle.kts | 7 + .../main/kotlin/pklScalaLibrary.gradle.kts | 53 ++++ gradle/libs.versions.toml | 10 + pkl-config-scala/NOTE.md | 44 +++ pkl-config-scala/pkl-config-scala.gradle.kts | 53 ++++ .../config/scala/annotation/EnumOwner.java | 11 + .../mapper/CachedConverterFactories.scala | 163 +++++++++++ .../scala/mapper/CachedSourceTypeInfo.scala | 78 ++++++ .../JavaReflectionSyntaxExtensions.scala | 131 +++++++++ .../mapper/PStringOrIntToEnumeration.scala | 34 +++ .../scala/mapper/ScalaConversions.scala | 79 ++++++ .../mapper/ScalaConverterFactories.scala | 242 ++++++++++++++++ .../org/pkl/config/scala/syntax/package.scala | 137 +++++++++ .../config/scala/mapper/PPairToScalaTuple.pkl | 7 + .../config/scala/ScalaObjectMapperSpec.scala | 260 ++++++++++++++++++ .../scala/mapper/PPairToScalaTupleSpec.scala | 73 +++++ settings.gradle.kts | 2 + 18 files changed, 1394 insertions(+), 1 deletion(-) create mode 100644 buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts create mode 100644 pkl-config-scala/NOTE.md create mode 100644 pkl-config-scala/pkl-config-scala.gradle.kts create mode 100644 pkl-config-scala/src/main/java/org/pkl/config/scala/annotation/EnumOwner.java create mode 100644 pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/CachedConverterFactories.scala create mode 100644 pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/CachedSourceTypeInfo.scala create mode 100644 pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/JavaReflectionSyntaxExtensions.scala create mode 100644 pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/PStringOrIntToEnumeration.scala create mode 100644 pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConversions.scala create mode 100644 pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConverterFactories.scala create mode 100644 pkl-config-scala/src/main/scala/org/pkl/config/scala/syntax/package.scala create mode 100644 pkl-config-scala/src/test/resources/org/pkl/config/scala/mapper/PPairToScalaTuple.pkl create mode 100644 pkl-config-scala/src/test/scala/org/pkl/config/scala/ScalaObjectMapperSpec.scala create mode 100644 pkl-config-scala/src/test/scala/org/pkl/config/scala/mapper/PPairToScalaTupleSpec.scala diff --git a/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts b/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts index 141fa8cac..9d36fafbc 100644 --- a/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts +++ b/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts @@ -26,11 +26,20 @@ dependencyLocking { lockAllConfigurations() } configurations { val rejectedVersionSuffix = Regex("-alpha|-beta|-eap|-m|-rc|-snapshot", RegexOption.IGNORE_CASE) + val versionSuffixRejectionExcemptions = setOf( + // I know. + // This looks odd. + // But yes, it's transitively required by one of the relese 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 { componentSelection { all { - if (rejectedVersionSuffix.containsMatchIn(candidate.version)) { + if (rejectedVersionSuffix.containsMatchIn(candidate.version) + && !versionSuffixRejectionExcemptions.contains("${candidate.group}:${candidate.module}:${candidate.version}") + ) { reject( "Rejected dependency $candidate " + "because it has a prelease version suffix matching `$rejectedVersionSuffix`." diff --git a/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts index 8540d3616..cd0dc13fd 100644 --- a/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts +++ b/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts @@ -15,6 +15,7 @@ */ @file:Suppress("HttpUrlsUsage", "unused") +import com.diffplug.gradle.spotless.JavaExtension import org.gradle.accessors.dm.LibrariesForLibs plugins { @@ -55,6 +56,12 @@ spotless { target("src/*/java/**/*.java") licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt")) } + scala { + scalafmt(libs.versions.scalafmt.get()) + target("src/*/scala/**/*.scala") + licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt"), + "package ") + } kotlin { ktfmt(libs.versions.ktfmt.get()).googleStyle() target("src/*/kotlin/**/*.kt") diff --git a/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts new file mode 100644 index 000000000..8053c7254 --- /dev/null +++ b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts @@ -0,0 +1,53 @@ +/* + * Copyright © 2024-2025 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 +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + + +plugins { + id("pklJavaLibrary") + scala +} + +// Build configuration. +val buildInfo = project.extensions.getByType() + +// Version Catalog library symbols. +val libs = the() + +dependencies { + api(libs.scalaLibrary) + testImplementation(libs.scalaTestPlusJunit) + testImplementation(libs.scalaTest) + testImplementation(libs.diffx) +} + +tasks.withType().configureEach { + compilerOptions { jvmTarget = JvmTarget.fromTarget(buildInfo.jvmTarget.toString()) } +} + +tasks.test { + useJUnitPlatform { + includeEngines("scalatest") + testLogging { + events("passed", "skipped", "failed") + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1b128a6bf..20dc630b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ assertj = "3.+" checksumPlugin = "1.4.0" clikt = "5.+" commonMark = "0.+" +diffx = "0.9.0" downloadTaskPlugin = "5.6.0" geantyref = "1.+" googleJavaFormat = "1.25.2" @@ -43,6 +44,10 @@ msgpack = "0.9.8" nexusPublishPlugin = "2.0.0" nuValidator = "20.+" paguro = "3.+" +scala = "2.13.16" +scalafmt = "3.9.6" +scalaTest = "3.2.19" +scalaTestPlusJunit = "3.2.19.0" shadowPlugin = "9.+" slf4j = "1.+" snakeYaml = "2.+" @@ -55,6 +60,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" } geantyref = { group = "io.leangen.geantyref", name = "geantyref", version.ref = "geantyref" } graalCompiler = { group = "org.graalvm.compiler", name = "compiler", version.ref = "graalVm" } @@ -87,6 +93,10 @@ nuValidator = { group = "nu.validator", name = "validator", version.ref = "nuVal # 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" } diff --git a/pkl-config-scala/NOTE.md b/pkl-config-scala/NOTE.md new file mode 100644 index 000000000..e901ea8d2 --- /dev/null +++ b/pkl-config-scala/NOTE.md @@ -0,0 +1,44 @@ +# Scala bindings for PKL language + +## Covered +- classes and case classes +- Scala `Option` for nullable PKL types +- Scala `Regexp` for PKL string/regexp +- Scala `Tuple2` for PKL Pair +- Scala `Duration` and `FiniteDuration` for PKL Duration +- Java `Instant` for PKL int and String (how about the rest of java.time?) +- Collections + - `immutable.Seq` + - `immutable.Vector` + - `immutable.List` + - `immutable.Set` + - `immutable.Map` + - `immutable.Stream` + - `immutable.LazyList` + - `mutable.Map` + - `mutable.Set` + - `mutable.Seq` + - `mutable.Buffer` + - `mutable.Queue` + - `mutable.Stack` +- Scala Enumeration (if annotated) + Scala 2 Enumeration is a runtime construct, we can't access it's members from Type referense. + To work around it, we introduced `@EnumOwner` annotation which you can use like below: + ``` + object SimpleEnum extends Enumeration { + + @EnumOwner(classOf[SimpleEnum.type]) + case class V() extends Val(nextId) + + val Aaa = V() + val Bbb = V() + val Ccc = V() + } + ``` + +## TODO +- more tests +- `Either` (???) +- `sealed traits` +- `object` instances +- cross-version compilation to cover scala `2.12` too diff --git a/pkl-config-scala/pkl-config-scala.gradle.kts b/pkl-config-scala/pkl-config-scala.gradle.kts new file mode 100644 index 000000000..30e1af7cf --- /dev/null +++ b/pkl-config-scala/pkl-config-scala.gradle.kts @@ -0,0 +1,53 @@ +/* + * Copyright © 2024-2025 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 { + pklAllProjects + pklScalaLibrary + pklFatJar + pklPublishLibrary + signing +} + +dependencies { + implementation(projects.pklConfigJava) + api(libs.scalaReflect) + firstPartySourcesJars(project(":pkl-core", "sourcesJar")) +} + +tasks.shadowJar { archiveBaseName.set("pkl-config-scala-all") } + +publishing { + publications { + named("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.") + } + } + + named("fatJar") { + artifactId = "pkl-config-scala-all" + pom { + url.set("https://github.com/apple/pkl/tree/main/pkl-config-scala") + description.set( + "Shaded fat Jar for pkl-config-scala, a Scala config library based on the Pkl config language." + ) + } + } + } +} + +signing { sign(publishing.publications["fatJar"]) } diff --git a/pkl-config-scala/src/main/java/org/pkl/config/scala/annotation/EnumOwner.java b/pkl-config-scala/src/main/java/org/pkl/config/scala/annotation/EnumOwner.java new file mode 100644 index 000000000..00148254c --- /dev/null +++ b/pkl-config-scala/src/main/java/org/pkl/config/scala/annotation/EnumOwner.java @@ -0,0 +1,11 @@ +package org.pkl.config.scala.annotation; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface EnumOwner { + + Class value(); + +} diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/CachedConverterFactories.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/CachedConverterFactories.scala new file mode 100644 index 000000000..8a416557f --- /dev/null +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/CachedConverterFactories.scala @@ -0,0 +1,163 @@ +/* + * Copyright © 2025 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)) } + ) +} diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/CachedSourceTypeInfo.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/CachedSourceTypeInfo.scala new file mode 100644 index 000000000..e0b9fac1d --- /dev/null +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/CachedSourceTypeInfo.scala @@ -0,0 +1,78 @@ +/* + * Copyright © 2025 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, ValueMapper} +import org.pkl.core.PClassInfo + +import java.lang.reflect.Type + +/** Manages cached type information and retrieves converters dynamically based + * on the type of input. + * + * `CachedSourceTypeInfo` encapsulates the source type information + * (`classInfo`) and a reusable converter, optimizing conversions by caching + * both type details and converters. This caching approach is particularly + * useful in repeated conversions where source type remains consistent. + */ +private[mapper] class CachedSourceTypeInfo { + + // Initially set to an unavailable type and will be updated based on the input value type. + private var classInfo: PClassInfo[Any] = + PClassInfo.Unavailable.asInstanceOf[PClassInfo[Any]] + + // Holds an optional converter, cached upon first retrieval. + private var converter: Option[Converter[Any, Any]] = None + + /** Updates the `classInfo` and retrieves a converter if the type of `v` + * differs from the cached `classInfo`. If the types match, the cached + * converter is reused. + * + * This method leverages caching to avoid redundant converter lookups, + * improving efficiency when the same type conversions are repeatedly + * required. + * + * @param v + * The value for which conversion is needed. + * @param t + * The target type to which the value should be converted. + * @param vm + * The `ValueMapper` responsible for providing the appropriate converter. + * + * @return + * The converted value, transformed to match the specified target type `t`. + * + * @example + * Basic usage: + * {{{ + * val cachedInfo = new CachedSourceTypeInfo() + * val result = cachedInfo.updateAndGet(myValue, targetType, myValueMapper) + * }}} + */ + def updateAndGet(v: Any, t: Type, vm: ValueMapper): Any = { + // Determine if the cached classInfo matches the type of v; if not, update and find new converter. + val c: Converter[Any, Any] = if (!classInfo.isExactClassOf(v)) { + classInfo = PClassInfo.forValue(v) + vm.getConverter(classInfo, t) + } else { + // Use the cached converter or obtain a new one if not cached yet. + converter getOrElse vm.getConverter(classInfo, t) + } + + converter = Some(c) // Cache the converter for subsequent conversions + c.convert(v, vm) // Convert and return the value + } +} diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/JavaReflectionSyntaxExtensions.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/JavaReflectionSyntaxExtensions.scala new file mode 100644 index 000000000..4bc3909a3 --- /dev/null +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/JavaReflectionSyntaxExtensions.scala @@ -0,0 +1,131 @@ +/* + * Copyright © 2025 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.Reflection +import org.pkl.config.scala.annotation.EnumOwner + +import java.lang.reflect.{GenericArrayType, ParameterizedType, Type} + +/** Provides aims to provide type-safe syntax extension to Java Reflection + * classes. + */ +private[mapper] object JavaReflectionSyntaxExtensions { + + /** `ParameterizedType` syntax extension. + */ + implicit class ParametrizedTypeSyntaxExtension(val x: Type) extends AnyVal { + + /** Retrieves the first type parameter of a `ParameterizedType`. + * + * @return + * The first `Type` parameter. + * + * @example + * Usage: + * {{{ + * val parameterizedType: ParameterizedType = // obtain a ParameterizedType instance + * val firstParamType = parameterizedType.params1 + * }}} + */ + def params1: Option[Type] = { + val tpe = x match { + case x: ParameterizedType => Some(x.getActualTypeArguments.apply(0)) + case x: GenericArrayType => Some(x.getGenericComponentType) + case x: Class[_] if x.isArray => Some(x.componentType()) + case _ => None + } + + tpe map Reflection.normalize + } + + /** Retrieves the first two type parameters of a `ParameterizedType`. + * + * @return + * A tuple containing the first and second `Type` parameters. + * + * @example + * Usage: + * {{{ + * val parameterizedType: ParameterizedType = // obtain a ParameterizedType instance + * val (firstParamType, secondParamType) = parameterizedType.params2 + * }}} + */ + def params2: Option[(Type, Type)] = x match { + case x: ParameterizedType => + Some( + ( + Reflection.normalize(x.getActualTypeArguments.apply(0)), + Reflection.normalize(x.getActualTypeArguments.apply(1)) + ) + ) + case _ => None + } + + /** + * Attempts to recover the full list of enumeration values from a given runtime class. + * + * This method is designed to work with Scala 2 `Enumeration` values that were defined + * using a custom subclass of `Enumeration.Val`, annotated with `@EnumOwner`, where the + * annotation holds a reference to the singleton `Enumeration` object. + * + * The method checks whether the provided `Type` is a subclass of + * `Enumeration#Value`, and if so, attempts to locate the `@EnumOwner` annotation on its class. + * If present, it uses reflection to access the singleton `Enumeration` instance and returns + * its list of values. + * + * @example + * {{{ + * object SimpleEnum extends Enumeration { + * + * @EnumOwner(classOf[SimpleEnum.type]) + * case class V() extends Val(nextId) + * + * val Aaa = V() + * val Bbb = V() + * val Ccc = V() + * } + * }}} + * + * @return Some(list of `Enumeration#Value`) if the enumeration can be resolved, None otherwise + */ + def asCustomEnum: Option[List[Enumeration#Value]] = { + + def derive(enumClass: Class[_]): Option[List[Enumeration#Value]] = { + try { + val f = enumClass.getDeclaredField("MODULE$") + f.setAccessible(true) + + val enumInstance = f.get(null).asInstanceOf[Enumeration] + Some(enumInstance.values.toList) + } catch { + case _: Throwable => None + } + } + + x match { + case x: Class[_] if classOf[Enumeration#Value].isAssignableFrom(x) => + for { + anno <- Option(x.getAnnotation(classOf[EnumOwner])) + enum <- derive(anno.value()) + } yield enum + + case _ => + None + } + } + } +} diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/PStringOrIntToEnumeration.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/PStringOrIntToEnumeration.scala new file mode 100644 index 000000000..ca7a83ab8 --- /dev/null +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/PStringOrIntToEnumeration.scala @@ -0,0 +1,34 @@ +package org.pkl.config.scala.mapper + +import org.pkl.config.java.mapper.{Converter, ConverterFactory, ValueMapper} +import org.pkl.config.scala.mapper.JavaReflectionSyntaxExtensions.ParametrizedTypeSyntaxExtension +import org.pkl.core.PClassInfo +import org.pkl.core.util.CodeGeneratorUtils + +import java.lang.reflect.Type +import java.util.Optional +import scala.jdk.OptionConverters._ + +private[mapper] object PStringOrIntToEnumeration extends ConverterFactory { + + override def create(sourceType: PClassInfo[_], targetType: Type): Optional[Converter[_, _]] = { + targetType.asCustomEnum + .map { members => + (new Converter[Any, Any] { + override def convert(value: Any, valueMapper: ValueMapper): Any = { + val res = value match { + case i: Long => members.collectFirst { + case value if value.id == i => value + } + case name: String => members.collectFirst { + case value if { val n = value.toString; n == name || CodeGeneratorUtils.toEnumConstantName(n).equals(name) } => value + } + case _ => None + } + + res.orNull + } + }).asInstanceOf[Converter[_, _]] + }.toJava + } +} diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConversions.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConversions.scala new file mode 100644 index 000000000..df03bf0fb --- /dev/null +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConversions.scala @@ -0,0 +1,79 @@ +/* + * Copyright © 2025 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.Conversion +import org.pkl.core.{PClassInfo, Duration => PDuration} + +import java.time.Instant +import java.util.concurrent.TimeUnit +import java.util.regex.Pattern +import scala.concurrent.duration.{Duration, FiniteDuration} +import scala.util.matching.Regex + +/** Provides conversions between Java types backing PKL and Scala types, + * enabling seamless interoperability for configuration values within PKL. + */ +object ScalaConversions { + + val pStringToInstant: Conversion[String, Instant] = + Conversion.of( + PClassInfo.String, + classOf[Instant], + (v: String, _) => Instant.parse(v) + ) + + val pIntToInstant: Conversion[java.lang.Long, Instant] = + Conversion.of( + PClassInfo.Int, + classOf[Instant], + (v: java.lang.Long, _) => Instant.ofEpochMilli(v) + ) + + val pDurationToDuration: Conversion[PDuration, Duration] = + Conversion.of( + PClassInfo.Duration, + classOf[Duration], + (v: PDuration, _) => Duration.fromNanos(v.inNanos()).toCoarsest + ) + + val pDurationToFiniteDuration: Conversion[PDuration, FiniteDuration] = + Conversion.of( + PClassInfo.Duration, + classOf[FiniteDuration], + (v: PDuration, _) => + FiniteDuration(v.inWholeNanos(), TimeUnit.NANOSECONDS).toCoarsest + ) + + val pStringToScalaRegex: Conversion[String, Regex] = + Conversion.of(PClassInfo.String, classOf[Regex], (v: String, _) => v.r) + + val pRegexToScalaRegex: Conversion[Pattern, Regex] = + Conversion.of( + PClassInfo.Regex, + classOf[Regex], + (v: Pattern, _) => v.pattern().r + ) + + def all: List[Conversion[_, _]] = List( + pIntToInstant, + pStringToInstant, + pDurationToFiniteDuration, + pDurationToDuration, + pStringToScalaRegex, + pRegexToScalaRegex + ) +} diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConverterFactories.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConverterFactories.scala new file mode 100644 index 000000000..8bf3c8577 --- /dev/null +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConverterFactories.scala @@ -0,0 +1,242 @@ +/* + * Copyright © 2025 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.{ConverterFactory, PObjectToDataObject, ValueMapper} +import org.pkl.config.scala.mapper.JavaReflectionSyntaxExtensions.ParametrizedTypeSyntaxExtension +import org.pkl.core.util.CodeGeneratorUtils +import org.pkl.core.{PClassInfo, PNull, PObject, Pair} + +import java.lang.reflect.{Constructor, Type} +import java.util.Optional +import scala.annotation.nowarn +import scala.collection.{immutable, mutable} +import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters._ +import scala.language.implicitConversions + +/** Defines a set of PKL to Scala converter factories. + */ +object ScalaConverterFactories { + + private type Conv1[S, T] = Type => (S, CachedSourceTypeInfo, ValueMapper) => T + + private type Conv2[S, T] = (Type, Type) => ( + S, + (CachedSourceTypeInfo, CachedSourceTypeInfo), + ValueMapper + ) => T + + val pObjectToCaseClass: ConverterFactory = new PObjectToDataObject { + + override def selectConstructor( + clazz: Class[_] + ): Optional[Constructor[_]] = { + clazz.getDeclaredConstructors.headOption + .filter(_ => + // case classes all implement Product + clazz.getInterfaces + .exists(i => classOf[scala.Product].isAssignableFrom(i)) + ) + .toJava + } + } + + val pAnyToOption: ConverterFactory = + CachedConverterFactories.forParametrizedType1[Any, Option[_]]( + _ => true, + t1 => + (value, s1, vm) => { + value match { + case _: PNull | null => None + case v: Option[_] => v.map(s1.updateAndGet(_, t1, vm)) + case v: Optional[_] => v.toScala.map(s1.updateAndGet(_, t1, vm)) + case v => Option(s1.updateAndGet(v, t1, vm)) + } + } + ) + + val pPairToTuple: ConverterFactory = + CachedConverterFactories.forParametrizedType2[Pair[_, _], (_, _)]( + PClassInfo.Pair, + (t1, t2) => + (value, cc, vm) => { + val (s1, s2) = cc + val p1 = s1.updateAndGet(value.getFirst, t1, vm) + val p2 = s2.updateAndGet(value.getSecond, t2, vm) + (p1, p2) + } + ) + + val pMapToMutableMapConv: Conv2[java.util.Map[_, _], mutable.Map[_, _]] = + (t1, t2) => + (value, cc, vm) => { + val (s1, s2) = cc + value.asScala.map { case (k, v) => + (s1.updateAndGet(k, t1, vm), s2.updateAndGet(v, t2, vm)) + } + } + + def pCollectionToMutableCollectionConv[T[_]]( + toSpecific: IterableOnce[_] => T[_] + ): Conv1[java.util.Collection[_], T[_]] = + t1 => + (value, cache, vm) => + toSpecific(value.asScala.map(x => cache.updateAndGet(x, t1, vm))) + + val pMapToImmutableMap: ConverterFactory = CachedConverterFactories + .forParametrizedType2[java.util.Map[_, _], immutable.Map[_, _]]( + PClassInfo.Map, + (t1, t2) => + (value, cc, vm) => pMapToMutableMapConv(t1, t2)(value, cc, vm).toMap + ) + + val pMapToMutableMap: ConverterFactory = CachedConverterFactories + .forParametrizedType2[java.util.Map[_, _], mutable.Map[_, _]]( + PClassInfo.Map, + pMapToMutableMapConv + ) + + val pObjectToImmutableMap: ConverterFactory = + CachedConverterFactories.forParametrizedType2[PObject, immutable.Map[_, _]]( + x => x == PClassInfo.Object | x == PClassInfo.Dynamic, + (t1, t2) => + (value, cc, vm) => + pMapToMutableMapConv(t1, t2)(value.getProperties, cc, vm).toMap + ) + + val pObjectToMutableMap: ConverterFactory = + CachedConverterFactories.forParametrizedType2[PObject, mutable.Map[_, _]]( + x => x == PClassInfo.Object | x == PClassInfo.Dynamic, + (t1, t2) => + (value, cc, vm) => + pMapToMutableMapConv(t1, t2)(value.getProperties, cc, vm) + ) + + // val pCollectionToArray: ConverterFactory = CachedConverterFactories + // .forParametrizedType1[java.util.Collection[_], Array[_]]( + // x => x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + // pCollectionToMutableCollectionConv[Array](_.iterator.toArray[Any]) + // ) + + val pCollectionToImmutableSet: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], immutable.Set[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(_.iterator.toSet) + ) + + val pCollectionToMutableSet: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], mutable.Set[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(mutable.Set.from) + ) + + val pCollectionToImmutableVector: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], immutable.Vector[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(_.iterator.toVector) + ) + + val pCollectionToImmutableSeq: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], immutable.Seq[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(_.iterator.toSeq) + ) + + val pCollectionToMutableSeq: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], mutable.Seq[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(mutable.Seq.from) + ) + + val pCollectionToMutableBuffer: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], mutable.Buffer[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(mutable.Buffer.from) + ) + + val pCollectionToImmutableList: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], immutable.List[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(_.iterator.toList) + ) + + val pCollectionToMutableQueue: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], mutable.Queue[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(mutable.Queue.from) + ) + + val pCollectionToMutableStack: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], mutable.Stack[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(mutable.Stack.from) + ) + + @nowarn("cat=deprecation") + val pCollectionToImmutableStream: ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], immutable.Stream[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(_.iterator.toStream) + ) + + val pCollectionToImmutableLazyList + : ConverterFactory = CachedConverterFactories + .forParametrizedType1[java.util.Collection[_], immutable.LazyList[_]]( + x => + x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, + pCollectionToMutableCollectionConv(_.iterator.to(LazyList)) + ) + + // Do not shuffle converter factories within this list. Order matters. + // As a general rule, try to keep more generic types lower and more specific higher + val all: List[ConverterFactory] = List( + pAnyToOption, + pPairToTuple, + pMapToImmutableMap, + pCollectionToImmutableStream, + pCollectionToImmutableSet, + pCollectionToImmutableList, + pCollectionToImmutableVector, + pCollectionToImmutableLazyList, + pCollectionToImmutableSeq, + pObjectToImmutableMap, + pMapToMutableMap, + pObjectToMutableMap, + pCollectionToMutableStack, + pCollectionToMutableSet, + pCollectionToMutableQueue, + pCollectionToMutableBuffer, + pCollectionToMutableSeq, + pObjectToCaseClass, + PStringOrIntToEnumeration + // pCollectionToArray, + ) + + private implicit def pClassInfoToPredicate( + x: PClassInfo[_] + ): PClassInfo[_] => Boolean = _ == x +} diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/syntax/package.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/syntax/package.scala new file mode 100644 index 000000000..780c81ee2 --- /dev/null +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/syntax/package.scala @@ -0,0 +1,137 @@ +/* + * Copyright © 2025 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 + +import org.pkl.config.java.mapper.{ConversionException, ValueMapperBuilder} +import org.pkl.config.java.{Config, ConfigEvaluator, ConfigEvaluatorBuilder} +import org.pkl.config.scala.mapper.{ScalaConversions, ScalaConverterFactories} + +import scala.jdk.CollectionConverters._ +import scala.reflect.ClassTag + +/** Entry point for Scala-specific extensions to PKL configuration, enabling + * type conversions and syntax improvements that align PKL's configuration + * model with Scala types and structures. + * + * The `syntax` package object introduces two main extensions: + * + * 1. `forScala`: Enhances the PKL evaluation stack by adding Scala-specific + * type conversions and converter factories, making it possible to work + * seamlessly with Scala types. + * 2. `Config.to`: Provides a type-safe `Config` conversion method. + */ +package object syntax { + + /** Extension for `ValueMapperBuilder`, enabling Scala-specific type + * conversions and factories. + * + * Adds conversions from `ScalaConversions` and converter factories from + * `ScalaConverterFactories` to the evaluation stack, allowing PKL to handle + * Scala-native types effectively. + * + * @example + * Using `forScala` with a ValueMapperBuilder: + * {{{ + * val builder = new ValueMapperBuilder().forScala() + * val evaluator = new ConfigEvaluatorBuilder().setValueMapperBuilder(builder).build + * }}} + */ + implicit class ValueMapperBuilderSyntaxExtension(val x: ValueMapperBuilder) + extends AnyVal { + def forScala(): ValueMapperBuilder = { + x.setConversions( + (ScalaConversions.all ++ x.getConversions.asScala).asJava + ).setConverterFactories( + (ScalaConverterFactories.all ++ x.getConverterFactories.asScala).asJava + ) + } + } + + /** Extension for `ConfigEvaluatorBuilder`, enabling Scala-specific type + * handling in the evaluator. + * + * This method sets up a `ConfigEvaluatorBuilder` with a `ValueMapperBuilder` + * that has been extended with Scala conversions, enabling the evaluator to + * process Scala-specific types in PKL configurations. + * + * @example + * Using `forScala` with a ConfigEvaluatorBuilder: + * {{{ + * val evaluatorBuilder = new ConfigEvaluatorBuilder().forScala() + * val evaluator = evaluatorBuilder.build + * }}} + */ + implicit class ConfigEvaluatorBuilderSyntaxExtension( + val x: ConfigEvaluatorBuilder + ) extends AnyVal { + def forScala(): ConfigEvaluatorBuilder = { + x.setValueMapperBuilder(x.getValueMapperBuilder.forScala()) + } + } + + /** Extension for `ConfigEvaluator`, applying Scala-specific type conversions + * to the evaluator. + * + * Builds a `ConfigEvaluator` with a Scala-aware `ValueMapper`, allowing for + * seamless conversion of configuration values to Scala types. + * + * @example + * Using `forScala` with a ConfigEvaluator: + * {{{ + * val evaluator = new ConfigEvaluatorBuilder().build.forScala() + * }}} + */ + implicit class ConfigEvaluatorSyntaxExtension(val x: ConfigEvaluator) + extends AnyVal { + def forScala(): ConfigEvaluator = { + x.setValueMapper(x.getValueMapper.toBuilder.forScala().build) + } + } + + /** Extension for `Config`, adding a type-safe `to` method to retrieve values + * as Scala types. + * + * The `to[T]` method provides an intuitive way to retrieve values from a PKL + * `Config` as specific Scala types. If a `null` is returned or the retrieved + * value does not match the target type, a `ConversionException` is thrown. + * This encourages the use of `Option` for nullable values in configurations. + * + * @param ct + * Implicit `ClassTag` of the target type `T`. + * + * @throws ConversionException + * if the value is `null` or does not match the specified type `T`. + * + * @example + * Retrieving a value as an Option: + * {{{ + * val myPklConfig: Config = // load or build a PKL config + * val config: MyScalaConfig = myPklConfig.to[MyCaseClass] + * }}} + */ + implicit class ConfigSyntaxExtension(val x: Config) extends AnyVal { + def to[T](implicit ct: ClassTag[T]): T = { + val result = x.as[T](ct.runtimeClass) + if (result == null || !result.isInstanceOf[T]) { + throw new ConversionException( + "Expected a non-null value but got `null`. " + + "To allow optional values, use `Option`. e.g. `Option[String]`." + ) + } + result + } + } +} diff --git a/pkl-config-scala/src/test/resources/org/pkl/config/scala/mapper/PPairToScalaTuple.pkl b/pkl-config-scala/src/test/resources/org/pkl/config/scala/mapper/PPairToScalaTuple.pkl new file mode 100644 index 000000000..432ea6455 --- /dev/null +++ b/pkl-config-scala/src/test/resources/org/pkl/config/scala/mapper/PPairToScalaTuple.pkl @@ -0,0 +1,7 @@ +class Person { + name: String + age: Int +} + +ex1 = Pair(1, 3.s) +ex2 = Pair(new Person {name = "pigeon"; age = 40}, new Dynamic {name = "parrot"; age = 30}) \ No newline at end of file diff --git a/pkl-config-scala/src/test/scala/org/pkl/config/scala/ScalaObjectMapperSpec.scala b/pkl-config-scala/src/test/scala/org/pkl/config/scala/ScalaObjectMapperSpec.scala new file mode 100644 index 000000000..594cd2f3b --- /dev/null +++ b/pkl-config-scala/src/test/scala/org/pkl/config/scala/ScalaObjectMapperSpec.scala @@ -0,0 +1,260 @@ +package org.pkl.config.scala + +import org.pkl.config.java.ConfigEvaluator +import org.pkl.config.scala.syntax._ +import org.scalatest.funsuite.AnyFunSuite +import org.pkl.core.{ModuleSource, Duration => PDuration} + +import java.time.{Instant, Duration => JDuration} +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.{Duration, FiniteDuration} +import com.softwaremill.diffx._ +import com.softwaremill.diffx.scalatest.DiffShouldMatcher._ +import org.pkl.config.scala.annotation.EnumOwner + +import scala.annotation.nowarn +import scala.collection.mutable + +@nowarn("cat=deprecation") +class ScalaObjectMapperSpec extends AnyFunSuite { + import ScalaObjectMapperSpec._ + + test("evaluate scala types") { + + val code = + """ + |module ObjectMappingTestContainer + | + |class Foo { + | value: Int + |} + | + |// Options + |optionalVal1: String? = null + |optionalVal2: String? = "some" + | + |// Instant + |instant1 = 0 + |instant2 = "2024-10-31T02:25:26.036Z" + | + |// Vector + |vector = List(1, 6, 9) + | + |// Seq + |seq = List(9, 5, 36, 1) + |mutableSeq = List("d", "a") + | + |// Buffer + |mutableBuffer = List("hoo", "ray") + | + |// Queue + |mutableQueue = Set("hoo", "ray") + | + |// Stack + |mutableStack = Set("hoo", "ray") + | + |// Duration + |pklDuration: Duration = 5.ms + |scalaFiniteDuration: Duration = 5.ms + |scalaDuration: Duration = 5000000.ns + | + |// Sets + |stringSet: Set = Set("in set") + |intSet: Set = Set(1,2,4,8,16,32) + |booleanSetSet: Set> = Set(Set(false), Set(true), Set(true, false)) + |mutableSet = Set("aaa", "cc", "b") + | + |// Lists + |stringList: List = List("in list") + |intList: List = List(1,2,3,5,7,11) + |booleanListList: List> = List(List(false), List(true), List(true, false)) + | + |// Streams + |stream: List = List("stream1", "stream2") + | + |// LazyList + |lazyList: List = List(5, 4, 7, 1) + | + |// Maps + |intStringMap: Map = Map(0, "in map") + |booleanIntStringMapMap: Map> = Map(false, Map(0, "in map in map")) + |booleanIntMapStringMap: Map, String> = Map(Map(true, 42), "in map with map keys") + | + |// Listings + |stringSetListing: Listing> = new { Set("in set in listing") } + |intListingListing: Listing> = new { new { 1337 } new { 100 } } + | + |// Mappings + |intStringMapping: Mapping = new { [42] = "in map" } + |stringStringSetMapping: Mapping> = new { ["key"] = Set("in set in map") } + | + |// Mutable Map + |mutableMap = Map("foo", "bar") + | + |// Map & Mappings with structured keys + |intSetListStringMap: Map>, String> = Map(List(Set(27)), "in map with structured key") + |typedStringMap: Map = Map( + | new Foo { value = 1 }, "using typed objects", + | new Foo { value = 2 }, "also works") + |dynamicStringMap: Map = Map( + | new Dynamic { value = 42 }, "using Dynamics", + | new Dynamic { hello = "world" }, "also works") + | + |intListingStringMapping: Mapping, String> = new { + | [new Listing { 42 1337 }] = "structured key works" + |} + |intSetListStringMapping: Mapping>, String> = new { + | [List(Set(27))] = "in mapping with structured key" + |} + |local intListing: Listing = new { 0 0 7 } + |thisOneGoesToEleven: Mapping>, Map, Mapping>> = new { + | [List(Set(0), Set(0), Set(7))] = Map(intListing, intStringMapping) + |} + | + |simpleEnumViaString = "Bbb" + |simpleEnumViaInt = 0 + |""".stripMargin + + val result = ConfigEvaluator + .preconfigured() + .forScala() + .evaluate(ModuleSource.text(code)) + .to[ObjectMappingTestContainer] + + result shouldMatchTo ObjectMappingTestContainer( + optionalVal1 = None, + optionalVal2 = Some("some"), + pklDuration = PDuration.ofMillis(5), + scalaDuration = Duration(5, TimeUnit.MILLISECONDS), + scalaFiniteDuration = FiniteDuration(5, TimeUnit.MILLISECONDS), + instant1 = Instant.ofEpochMilli(0), + instant2 = Instant.parse("2024-10-31T02:25:26.036Z"), + stringSet = Set("in set"), + intSet = Set(1, 2, 32, 16, 8, 4), + booleanSetSet = Set(Set(false), Set(true), Set(true, false)), + stringList = List("in list"), + intList = List(1, 2, 3, 5, 7, 11), + booleanListList = List(List(false), List(true), List(true, false)), + vector = Vector(1, 6, 9), + seq = Seq(9, 5, 36, 1), + stream = Stream("stream1", "stream2"), + lazyList = LazyList(5, 4, 7, 1), + intStringMap = Map(0 -> "in map"), + booleanIntStringMapMap = Map(false -> Map(0 -> "in map in map")), + booleanIntMapStringMap = Map(Map(true -> 42) -> "in map with map keys"), + intSetListStringMap = Map(List(Set(27)) -> "in map with structured key"), + typedStringMap = Map( + TypedKey(1) -> "using typed objects", + TypedKey(2) -> "also works" + ), + dynamicStringMap = Map( + Map("value" -> 42) -> "using Dynamics", + Map("hello" -> "world") -> "also works" + ), + mutableMap = mutable.Map("foo" -> "bar"), + mutableSet = mutable.Set("cc", "aaa", "b"), + mutableSeq = mutable.Seq("d", "a"), + mutableBuffer = mutable.Buffer("hoo", "ray"), + mutableQueue = mutable.Queue("hoo", "ray"), + mutableStack = mutable.Stack("hoo", "ray"), + stringSetListing = List(Set("in set in listing")), + intListingListing = List(List(1337), List(100)), + intStringMapping = Map(42 -> "in map"), + stringStringSetMapping = Map("key" -> Set("in set in map")), + intListingStringMapping = Map(List(42, 1337) -> "structured key works"), + intSetListStringMapping = + Map(List(Set(27)) -> "in mapping with structured key"), + thisOneGoesToEleven = Map( + List(Set(0), Set(0), Set(7)) -> Map( + List(0, 0, 7) -> Map(42 -> "in map") + ) + ), + simpleEnumViaString = SimpleEnum.Bbb, + simpleEnumViaInt = SimpleEnum.Aaa + ) + } +} + +@nowarn("cat=deprecation") +object ScalaObjectMapperSpec { + + case class TypedKey(value: Int) + object TypedKey { + implicit val diffx: Diff[TypedKey] = Diff.derived[TypedKey] + } + + object SimpleEnum extends Enumeration { + @EnumOwner(classOf[SimpleEnum.type]) + case class V() extends Val(nextId) + + val Aaa = V() + val Bbb = V() + val Ccc = V() + } + + case class ObjectMappingTestContainer( + // Options + optionalVal1: Option[String], + optionalVal2: Option[String], + // Duration + pklDuration: PDuration, + scalaFiniteDuration: FiniteDuration, + scalaDuration: Duration, + // Instant + instant1: Instant, + instant2: Instant, + // Sets + stringSet: Set[String], + intSet: Set[Int], + booleanSetSet: Set[Set[Boolean]], + // Lists + stringList: List[String], + intList: List[Int], + booleanListList: List[List[Boolean]], + // Stream + stream: Stream[String], + // LazyList + lazyList: LazyList[Int], + // Vector + vector: Vector[Int], + // Seq + seq: Seq[Int], + // Maps + intStringMap: Map[Int, String], + booleanIntStringMapMap: Map[Boolean, Map[Int, String]], + booleanIntMapStringMap: Map[Map[Boolean, Int], String], + intSetListStringMap: Map[List[Set[Int]], String], + typedStringMap: Map[TypedKey, String], + dynamicStringMap: Map[Map[String, Any], String], + // mutable.Map + mutableMap: mutable.Map[String, String], + // mutable.Set + mutableSet: mutable.Set[String], + // mutable.Seq + mutableSeq: mutable.Seq[String], + // mutable.Buffer + mutableBuffer: mutable.Buffer[String], + // mutable.Queue + mutableQueue: mutable.Queue[String], + // mutable.Stack + mutableStack: mutable.Stack[String], + // Listings + stringSetListing: List[Set[String]], + intListingListing: List[List[Int]], + // Mapping + intStringMapping: Map[Int, String], + stringStringSetMapping: Map[String, Set[String]], + // Map & Mapping with structured keys + intListingStringMapping: Map[List[Int], String], + intSetListStringMapping: Map[List[Set[Int]], String], + thisOneGoesToEleven: Map[List[Set[Int]], Map[List[Int], Map[Int, String]]], + // enums + simpleEnumViaString: SimpleEnum.V, + simpleEnumViaInt: SimpleEnum.V + ) + + object ObjectMappingTestContainer { + implicit def anyDiffx[T]: Diff[T] = Diff.useEquals[T] + implicit val diffx: Diff[ObjectMappingTestContainer] = Diff.derived[ObjectMappingTestContainer] + } +} diff --git a/pkl-config-scala/src/test/scala/org/pkl/config/scala/mapper/PPairToScalaTupleSpec.scala b/pkl-config-scala/src/test/scala/org/pkl/config/scala/mapper/PPairToScalaTupleSpec.scala new file mode 100644 index 000000000..4b9884c5e --- /dev/null +++ b/pkl-config-scala/src/test/scala/org/pkl/config/scala/mapper/PPairToScalaTupleSpec.scala @@ -0,0 +1,73 @@ +package org.pkl.config.scala.mapper + +import org.pkl.config.java.mapper.{Types, ValueMapperBuilder} +import org.pkl.core.{Duration, Evaluator, PClassInfo, PObject} +import org.pkl.core.ModuleSource.modulePath +import org.scalatest.BeforeAndAfterAll +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers._ +import org.pkl.config.scala.syntax._ + +import scala.jdk.CollectionConverters._ + +class PPairToScalaTupleSpec extends AnyFunSuite with BeforeAndAfterAll { + import PPairToScalaTupleSpec._ + + private val evaluator = Evaluator.preconfigured() + + private val module = + evaluator.evaluate(modulePath("org/pkl/config/scala/mapper/PPairToScalaTuple.pkl")) + + private val mapper = ValueMapperBuilder.preconfigured().forScala().build() + + override def afterAll(): Unit = { + evaluator.close() + } + + test("Pair or scalar values") { + val ex1 = module.getProperty("ex1") + val mapped: (Int, Duration) = + mapper.map( + ex1, + Types.parameterizedType(classOf[Tuple2[_, _]], classOf[Integer], classOf[Duration]) + ) + + mapped shouldBe (1, Duration.ofSeconds(3)) + } + + test("Pair of PObject") { + val ex2 = module.getProperty("ex2") + val mapped: (PObject, PObject) = + mapper.map( + ex2, + Types.parameterizedType(classOf[Tuple2[_, _]], classOf[PObject], classOf[PObject]) + ) + + mapped._1.getProperties.asScala should contain only ( + "name" -> "pigeon", + "age" -> 40L + ) + + mapped._2.getProperties.asScala should contain only ( + "name" -> "parrot", + "age" -> 30L + ) + } + + test("Pair of case class") { + val ex2 = module.getProperty("ex2") + val mapped: (Animal, Animal) = + mapper.map( + ex2, + Types.parameterizedType(classOf[Tuple2[_, _]], classOf[Animal], classOf[Animal]) + ) + + mapped._1 shouldBe Animal("pigeon", 40L) + mapped._2 shouldBe Animal("parrot", 30L) + } +} + +object PPairToScalaTupleSpec { + + case class Animal(name: String, age: Long) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index de11c8314..c6906a4a1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,8 @@ include("pkl-config-java") include("pkl-config-kotlin") +include("pkl-config-scala") + include("pkl-core") include("pkl-doc") From 9ffad63321ba5d66b7b814d8483295f8d437335c Mon Sep 17 00:00:00 2001 From: Andriy Onyshchuk Date: Fri, 23 May 2025 14:32:50 -0700 Subject: [PATCH 2/7] fix style --- .../config/scala/annotation/EnumOwner.java | 16 ++++- .../JavaReflectionSyntaxExtensions.scala | 61 ++++++++++--------- .../mapper/PStringOrIntToEnumeration.scala | 55 ++++++++++++----- .../mapper/ScalaConverterFactories.scala | 32 +++++----- .../config/scala/ScalaObjectMapperSpec.scala | 29 +++++++-- .../scala/mapper/PPairToScalaTupleSpec.scala | 45 +++++++++++--- 6 files changed, 166 insertions(+), 72 deletions(-) diff --git a/pkl-config-scala/src/main/java/org/pkl/config/scala/annotation/EnumOwner.java b/pkl-config-scala/src/main/java/org/pkl/config/scala/annotation/EnumOwner.java index 00148254c..7b0bb01ed 100644 --- a/pkl-config-scala/src/main/java/org/pkl/config/scala/annotation/EnumOwner.java +++ b/pkl-config-scala/src/main/java/org/pkl/config/scala/annotation/EnumOwner.java @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 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.annotation; import java.lang.annotation.*; @@ -7,5 +22,4 @@ public @interface EnumOwner { Class value(); - } diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/JavaReflectionSyntaxExtensions.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/JavaReflectionSyntaxExtensions.scala index 4bc3909a3..62fad6a68 100644 --- a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/JavaReflectionSyntaxExtensions.scala +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/JavaReflectionSyntaxExtensions.scala @@ -75,33 +75,36 @@ private[mapper] object JavaReflectionSyntaxExtensions { case _ => None } - /** - * Attempts to recover the full list of enumeration values from a given runtime class. - * - * This method is designed to work with Scala 2 `Enumeration` values that were defined - * using a custom subclass of `Enumeration.Val`, annotated with `@EnumOwner`, where the - * annotation holds a reference to the singleton `Enumeration` object. - * - * The method checks whether the provided `Type` is a subclass of - * `Enumeration#Value`, and if so, attempts to locate the `@EnumOwner` annotation on its class. - * If present, it uses reflection to access the singleton `Enumeration` instance and returns - * its list of values. - * - * @example - * {{{ - * object SimpleEnum extends Enumeration { - * - * @EnumOwner(classOf[SimpleEnum.type]) - * case class V() extends Val(nextId) - * - * val Aaa = V() - * val Bbb = V() - * val Ccc = V() - * } - * }}} - * - * @return Some(list of `Enumeration#Value`) if the enumeration can be resolved, None otherwise - */ + /** Attempts to recover the full list of enumeration values from a given + * runtime class. + * + * This method is designed to work with Scala 2 `Enumeration` values that + * were defined using a custom subclass of `Enumeration.Val`, annotated + * with `@EnumOwner`, where the annotation holds a reference to the + * singleton `Enumeration` object. + * + * The method checks whether the provided `Type` is a subclass of + * `Enumeration#Value`, and if so, attempts to locate the `@EnumOwner` + * annotation on its class. If present, it uses reflection to access the + * singleton `Enumeration` instance and returns its list of values. + * + * @example + * {{{ + * object SimpleEnum extends Enumeration { + * + * @EnumOwner(classOf[SimpleEnum.type]) + * case class V() extends Val(nextId) + * + * val Aaa = V() + * val Bbb = V() + * val Ccc = V() + * } + * }}} + * + * @return + * Some(list of `Enumeration#Value`) if the enumeration can be resolved, + * None otherwise + */ def asCustomEnum: Option[List[Enumeration#Value]] = { def derive(enumClass: Class[_]): Option[List[Enumeration#Value]] = { @@ -115,14 +118,14 @@ private[mapper] object JavaReflectionSyntaxExtensions { case _: Throwable => None } } - + x match { case x: Class[_] if classOf[Enumeration#Value].isAssignableFrom(x) => for { anno <- Option(x.getAnnotation(classOf[EnumOwner])) enum <- derive(anno.value()) } yield enum - + case _ => None } diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/PStringOrIntToEnumeration.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/PStringOrIntToEnumeration.scala index ca7a83ab8..50d5bd861 100644 --- a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/PStringOrIntToEnumeration.scala +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/PStringOrIntToEnumeration.scala @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 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, ValueMapper} @@ -11,24 +26,34 @@ import scala.jdk.OptionConverters._ private[mapper] object PStringOrIntToEnumeration extends ConverterFactory { - override def create(sourceType: PClassInfo[_], targetType: Type): Optional[Converter[_, _]] = { - targetType.asCustomEnum - .map { members => - (new Converter[Any, Any] { - override def convert(value: Any, valueMapper: ValueMapper): Any = { - val res = value match { - case i: Long => members.collectFirst { + override def create( + sourceType: PClassInfo[_], + targetType: Type + ): Optional[Converter[_, _]] = { + targetType.asCustomEnum.map { members => + (new Converter[Any, Any] { + override def convert(value: Any, valueMapper: ValueMapper): Any = { + val res = value match { + case i: Long => + members.collectFirst { case value if value.id == i => value } - case name: String => members.collectFirst { - case value if { val n = value.toString; n == name || CodeGeneratorUtils.toEnumConstantName(n).equals(name) } => value + case name: String => + members.collectFirst { + case value if { + val n = value.toString; + n == name || CodeGeneratorUtils + .toEnumConstantName(n) + .equals(name) + } => + value } - case _ => None - } - - res.orNull + case _ => None } - }).asInstanceOf[Converter[_, _]] - }.toJava + + res.orNull + } + }).asInstanceOf[Converter[_, _]] + }.toJava } } diff --git a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConverterFactories.scala b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConverterFactories.scala index 8bf3c8577..515386f37 100644 --- a/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConverterFactories.scala +++ b/pkl-config-scala/src/main/scala/org/pkl/config/scala/mapper/ScalaConverterFactories.scala @@ -15,7 +15,11 @@ */ package org.pkl.config.scala.mapper -import org.pkl.config.java.mapper.{ConverterFactory, PObjectToDataObject, ValueMapper} +import org.pkl.config.java.mapper.{ + ConverterFactory, + PObjectToDataObject, + ValueMapper +} import org.pkl.config.scala.mapper.JavaReflectionSyntaxExtensions.ParametrizedTypeSyntaxExtension import org.pkl.core.util.CodeGeneratorUtils import org.pkl.core.{PClassInfo, PNull, PObject, Pair} @@ -29,22 +33,22 @@ import scala.jdk.OptionConverters._ import scala.language.implicitConversions /** Defines a set of PKL to Scala converter factories. - */ + */ object ScalaConverterFactories { private type Conv1[S, T] = Type => (S, CachedSourceTypeInfo, ValueMapper) => T private type Conv2[S, T] = (Type, Type) => ( - S, + S, (CachedSourceTypeInfo, CachedSourceTypeInfo), ValueMapper - ) => T + ) => T val pObjectToCaseClass: ConverterFactory = new PObjectToDataObject { override def selectConstructor( - clazz: Class[_] - ): Optional[Constructor[_]] = { + clazz: Class[_] + ): Optional[Constructor[_]] = { clazz.getDeclaredConstructors.headOption .filter(_ => // case classes all implement Product @@ -62,9 +66,9 @@ object ScalaConverterFactories { (value, s1, vm) => { value match { case _: PNull | null => None - case v: Option[_] => v.map(s1.updateAndGet(_, t1, vm)) - case v: Optional[_] => v.toScala.map(s1.updateAndGet(_, t1, vm)) - case v => Option(s1.updateAndGet(v, t1, vm)) + case v: Option[_] => v.map(s1.updateAndGet(_, t1, vm)) + case v: Optional[_] => v.toScala.map(s1.updateAndGet(_, t1, vm)) + case v => Option(s1.updateAndGet(v, t1, vm)) } } ) @@ -91,8 +95,8 @@ object ScalaConverterFactories { } def pCollectionToMutableCollectionConv[T[_]]( - toSpecific: IterableOnce[_] => T[_] - ): Conv1[java.util.Collection[_], T[_]] = + toSpecific: IterableOnce[_] => T[_] + ): Conv1[java.util.Collection[_], T[_]] = t1 => (value, cache, vm) => toSpecific(value.asScala.map(x => cache.updateAndGet(x, t1, vm))) @@ -204,7 +208,7 @@ object ScalaConverterFactories { ) val pCollectionToImmutableLazyList - : ConverterFactory = CachedConverterFactories + : ConverterFactory = CachedConverterFactories .forParametrizedType1[java.util.Collection[_], immutable.LazyList[_]]( x => x == PClassInfo.Collection | x == PClassInfo.Set | x == PClassInfo.List, @@ -237,6 +241,6 @@ object ScalaConverterFactories { ) private implicit def pClassInfoToPredicate( - x: PClassInfo[_] - ): PClassInfo[_] => Boolean = _ == x + x: PClassInfo[_] + ): PClassInfo[_] => Boolean = _ == x } diff --git a/pkl-config-scala/src/test/scala/org/pkl/config/scala/ScalaObjectMapperSpec.scala b/pkl-config-scala/src/test/scala/org/pkl/config/scala/ScalaObjectMapperSpec.scala index 594cd2f3b..35d0c60a3 100644 --- a/pkl-config-scala/src/test/scala/org/pkl/config/scala/ScalaObjectMapperSpec.scala +++ b/pkl-config-scala/src/test/scala/org/pkl/config/scala/ScalaObjectMapperSpec.scala @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 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 import org.pkl.config.java.ConfigEvaluator @@ -20,7 +35,7 @@ class ScalaObjectMapperSpec extends AnyFunSuite { import ScalaObjectMapperSpec._ test("evaluate scala types") { - + val code = """ |module ObjectMappingTestContainer @@ -186,12 +201,12 @@ object ScalaObjectMapperSpec { object SimpleEnum extends Enumeration { @EnumOwner(classOf[SimpleEnum.type]) case class V() extends Val(nextId) - + val Aaa = V() val Bbb = V() val Ccc = V() } - + case class ObjectMappingTestContainer( // Options optionalVal1: Option[String], @@ -247,7 +262,10 @@ object ScalaObjectMapperSpec { // Map & Mapping with structured keys intListingStringMapping: Map[List[Int], String], intSetListStringMapping: Map[List[Set[Int]], String], - thisOneGoesToEleven: Map[List[Set[Int]], Map[List[Int], Map[Int, String]]], + thisOneGoesToEleven: Map[ + List[Set[Int]], + Map[List[Int], Map[Int, String]] + ], // enums simpleEnumViaString: SimpleEnum.V, simpleEnumViaInt: SimpleEnum.V @@ -255,6 +273,7 @@ object ScalaObjectMapperSpec { object ObjectMappingTestContainer { implicit def anyDiffx[T]: Diff[T] = Diff.useEquals[T] - implicit val diffx: Diff[ObjectMappingTestContainer] = Diff.derived[ObjectMappingTestContainer] + implicit val diffx: Diff[ObjectMappingTestContainer] = + Diff.derived[ObjectMappingTestContainer] } } diff --git a/pkl-config-scala/src/test/scala/org/pkl/config/scala/mapper/PPairToScalaTupleSpec.scala b/pkl-config-scala/src/test/scala/org/pkl/config/scala/mapper/PPairToScalaTupleSpec.scala index 4b9884c5e..29859daae 100644 --- a/pkl-config-scala/src/test/scala/org/pkl/config/scala/mapper/PPairToScalaTupleSpec.scala +++ b/pkl-config-scala/src/test/scala/org/pkl/config/scala/mapper/PPairToScalaTupleSpec.scala @@ -1,3 +1,18 @@ +/* + * Copyright © 2025 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.{Types, ValueMapperBuilder} @@ -16,7 +31,9 @@ class PPairToScalaTupleSpec extends AnyFunSuite with BeforeAndAfterAll { private val evaluator = Evaluator.preconfigured() private val module = - evaluator.evaluate(modulePath("org/pkl/config/scala/mapper/PPairToScalaTuple.pkl")) + evaluator.evaluate( + modulePath("org/pkl/config/scala/mapper/PPairToScalaTuple.pkl") + ) private val mapper = ValueMapperBuilder.preconfigured().forScala().build() @@ -29,8 +46,12 @@ class PPairToScalaTupleSpec extends AnyFunSuite with BeforeAndAfterAll { val mapped: (Int, Duration) = mapper.map( ex1, - Types.parameterizedType(classOf[Tuple2[_, _]], classOf[Integer], classOf[Duration]) - ) + Types.parameterizedType( + classOf[Tuple2[_, _]], + classOf[Integer], + classOf[Duration] + ) + ) mapped shouldBe (1, Duration.ofSeconds(3)) } @@ -40,8 +61,12 @@ class PPairToScalaTupleSpec extends AnyFunSuite with BeforeAndAfterAll { val mapped: (PObject, PObject) = mapper.map( ex2, - Types.parameterizedType(classOf[Tuple2[_, _]], classOf[PObject], classOf[PObject]) - ) + Types.parameterizedType( + classOf[Tuple2[_, _]], + classOf[PObject], + classOf[PObject] + ) + ) mapped._1.getProperties.asScala should contain only ( "name" -> "pigeon", @@ -59,8 +84,12 @@ class PPairToScalaTupleSpec extends AnyFunSuite with BeforeAndAfterAll { val mapped: (Animal, Animal) = mapper.map( ex2, - Types.parameterizedType(classOf[Tuple2[_, _]], classOf[Animal], classOf[Animal]) - ) + Types.parameterizedType( + classOf[Tuple2[_, _]], + classOf[Animal], + classOf[Animal] + ) + ) mapped._1 shouldBe Animal("pigeon", 40L) mapped._2 shouldBe Animal("parrot", 30L) @@ -70,4 +99,4 @@ class PPairToScalaTupleSpec extends AnyFunSuite with BeforeAndAfterAll { object PPairToScalaTupleSpec { case class Animal(name: String, age: Long) -} \ No newline at end of file +} From bb90a584b78425f9387e942d6bcaca4261b1d33c Mon Sep 17 00:00:00 2001 From: Andriy Onyshchuk Date: Fri, 23 May 2025 21:10:10 -0700 Subject: [PATCH 3/7] fix style --- .../src/main/kotlin/pklAllProjects.gradle.kts | 22 +++++++++++-------- .../src/main/kotlin/pklJavaLibrary.gradle.kts | 7 +++--- .../main/kotlin/pklScalaLibrary.gradle.kts | 5 +---- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts b/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts index 9d36fafbc..5ab9671e9 100644 --- a/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts +++ b/buildSrc/src/main/kotlin/pklAllProjects.gradle.kts @@ -26,19 +26,23 @@ dependencyLocking { lockAllConfigurations() } configurations { val rejectedVersionSuffix = Regex("-alpha|-beta|-eap|-m|-rc|-snapshot", RegexOption.IGNORE_CASE) - val versionSuffixRejectionExcemptions = setOf( - // I know. - // This looks odd. - // But yes, it's transitively required by one of the relese versions of `zinc` - // https://github.com/sbt/zinc/blame/57a2df7104b3ce27b46404bb09a0126bd4013427/project/Dependencies.scala#L85 - "com.eed3si9n:shaded-scalajson_2.13:1.0.0-M4" - ) + val versionSuffixRejectionExcemptions = + setOf( + // I know. + // This looks odd. + // But yes, it's transitively required by one of the relese 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 { componentSelection { all { - if (rejectedVersionSuffix.containsMatchIn(candidate.version) - && !versionSuffixRejectionExcemptions.contains("${candidate.group}:${candidate.module}:${candidate.version}") + if ( + rejectedVersionSuffix.containsMatchIn(candidate.version) && + !versionSuffixRejectionExcemptions.contains( + "${candidate.group}:${candidate.module}:${candidate.version}" + ) ) { reject( "Rejected dependency $candidate " + diff --git a/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts index cd0dc13fd..2a8f4e8d5 100644 --- a/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts +++ b/buildSrc/src/main/kotlin/pklJavaLibrary.gradle.kts @@ -15,7 +15,6 @@ */ @file:Suppress("HttpUrlsUsage", "unused") -import com.diffplug.gradle.spotless.JavaExtension import org.gradle.accessors.dm.LibrariesForLibs plugins { @@ -59,8 +58,10 @@ spotless { scala { scalafmt(libs.versions.scalafmt.get()) target("src/*/scala/**/*.scala") - licenseHeaderFile(rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt"), - "package ") + licenseHeaderFile( + rootProject.file("buildSrc/src/main/resources/license-header.star-block.txt"), + "package ", + ) } kotlin { ktfmt(libs.versions.ktfmt.get()).googleStyle() diff --git a/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts index 8053c7254..4f601e75f 100644 --- a/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts +++ b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts @@ -20,7 +20,6 @@ import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile - plugins { id("pklJavaLibrary") scala @@ -46,8 +45,6 @@ tasks.withType().configureEach { tasks.test { useJUnitPlatform { includeEngines("scalatest") - testLogging { - events("passed", "skipped", "failed") - } + testLogging { events("passed", "skipped", "failed") } } } From 6a9b40e57a69adbb1f35e6bd8e14f2d3bb581deb Mon Sep 17 00:00:00 2001 From: Andriy Onyshchuk Date: Wed, 15 Oct 2025 11:06:19 -0700 Subject: [PATCH 4/7] remove pkl-config-scala from fatJar and signing flows --- pkl-config-scala/pkl-config-scala.gradle.kts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pkl-config-scala/pkl-config-scala.gradle.kts b/pkl-config-scala/pkl-config-scala.gradle.kts index 30e1af7cf..c22187a82 100644 --- a/pkl-config-scala/pkl-config-scala.gradle.kts +++ b/pkl-config-scala/pkl-config-scala.gradle.kts @@ -16,19 +16,14 @@ plugins { pklAllProjects pklScalaLibrary - pklFatJar pklPublishLibrary - signing } dependencies { implementation(projects.pklConfigJava) api(libs.scalaReflect) - firstPartySourcesJars(project(":pkl-core", "sourcesJar")) } -tasks.shadowJar { archiveBaseName.set("pkl-config-scala-all") } - publishing { publications { named("library") { @@ -37,17 +32,5 @@ publishing { description.set("Scala config library based on the Pkl config language.") } } - - named("fatJar") { - artifactId = "pkl-config-scala-all" - pom { - url.set("https://github.com/apple/pkl/tree/main/pkl-config-scala") - description.set( - "Shaded fat Jar for pkl-config-scala, a Scala config library based on the Pkl config language." - ) - } - } } } - -signing { sign(publishing.publications["fatJar"]) } From aa93e0e28700166c75fa9652895629aa1aedf4ef Mon Sep 17 00:00:00 2001 From: Andriy Onyshchuk Date: Wed, 15 Oct 2025 12:38:20 -0700 Subject: [PATCH 5/7] update to support scala 3 --- buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts index 4f601e75f..1a3979d78 100644 --- a/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts +++ b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts @@ -38,8 +38,10 @@ dependencies { testImplementation(libs.diffx) } -tasks.withType().configureEach { - compilerOptions { jvmTarget = JvmTarget.fromTarget(buildInfo.jvmTarget.toString()) } +tasks.withType().configureEach { + scalaCompileOptions.apply { + additionalParameters = listOf("-Xsource:3") + } } tasks.test { From 11f1d47abac16ba297436fd985c5cddd90cfe01f Mon Sep 17 00:00:00 2001 From: Andriy Onyshchuk Date: Wed, 15 Oct 2025 13:30:23 -0700 Subject: [PATCH 6/7] fix gradle style --- buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts index 1a3979d78..c3b379dbe 100644 --- a/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts +++ b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts @@ -17,8 +17,6 @@ import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { id("pklJavaLibrary") @@ -39,9 +37,7 @@ dependencies { } tasks.withType().configureEach { - scalaCompileOptions.apply { - additionalParameters = listOf("-Xsource:3") - } + scalaCompileOptions.apply { additionalParameters = listOf("-Xsource:3") } } tasks.test { From b6a58a12c64c2d49c3a8b42ae079d2448341dfde Mon Sep 17 00:00:00 2001 From: Andriy Onyshchuk Date: Wed, 15 Oct 2025 23:39:19 -0700 Subject: [PATCH 7/7] fix multi-jvm build --- buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts | 3 ++- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts index c3b379dbe..df6b5a7da 100644 --- a/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts +++ b/buildSrc/src/main/kotlin/pklScalaLibrary.gradle.kts @@ -37,7 +37,8 @@ dependencies { } tasks.withType().configureEach { - scalaCompileOptions.apply { additionalParameters = listOf("-Xsource:3") } + scalaCompileOptions.additionalParameters = + listOf("-Xsource:3", "-release:${buildInfo.jvmTarget}", "-target:${buildInfo.jvmTarget}") } tasks.test { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 20dc630b2..83923b8f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,8 +44,8 @@ msgpack = "0.9.8" nexusPublishPlugin = "2.0.0" nuValidator = "20.+" paguro = "3.+" -scala = "2.13.16" -scalafmt = "3.9.6" +scala = "2.13.17" +scalafmt = "3.10.0" scalaTest = "3.2.19" scalaTestPlusJunit = "3.2.19.0" shadowPlugin = "9.+"