A library and gradle plugin for generating kotlin extension function wrappers around scala libraries with extensions
The project is split into two main modules:
codegen
- The library that generates the Kotlin extension functions given some configurationplugin-build
- The Gradle plugin that uses thecodegen
library to generate Kotlin extension wrapper functions for Scala libraries
Add the plugin to your build.gradle.kts
:
plugins {
alias(libs.plugins.kotlin)
id("com.stackgen.codegen.scalakotlin")
}
...and configure the generateKotlinExtensionsForScalaClasses
task:
tasks {
generateKotlinExtensionsForScalaClasses {
outputDirectory = file(generatedSourcesDir)
}
}
The plugin is not yet available on the Gradle Plugin Portal/Maven central. It is currently only available in the Maven GitHub Package Registry.
Github Packages' Maven registry requires authentication, so it is recommended to generate a Personal Access Token with packages:read
scope.
Add the following repository to your settings.gradle.kts
:
pluginManagement {
repositories {
gradlePluginPortal()
maven("https://maven.pkg.github.com/stackgenhq/scala-kotlin-extension-codegen") {
name = "Scala-Kotlin Extension Codegen GPR"
credentials {
username = providers.gradleProperty("gpr.user").getOrElse(System.getenv("GPR_USER"))
password = providers.gradleProperty("gpr.key").getOrElse(System.getenv("GPR_KEY"))
}
}
}
}
...and add those keys to your gradle.properties
or environment. See Configuring the Build Environment for more on configuring Gradle properties.
The plugin creates a codegen
configuration, where scala dependencies can be added for the plugin to scan for extensions in order to generate wrapper kotlin extension functions.
Note that for the generated code to be compilable (as it calls into the scala libraries), the classes must be available on the compilation classpath.
If these generated wrappers are to be used in the same project, you can either add the dependencies to the necessary configurations manually or extend them from codegen
, for example:
build.gradle.kts
// codegen deps must be available on classpath at runtime for the classloader, at compile time for the generated code
configurations.implementation.extendsFrom(configurations.codegen)
Then, simply add codegen
dependencies:
dependencies {
codegen("com.outr:scribe_3:3.15.0")
}
The task can be configured to scan a directory of Scala classes for extensions in order to generate wrapper kotlin extension functions. For example:
generateKotlinExtensionsForScalaClasses {
classesDir = file("build/classes/scala/main")
// ...other configuration
}
If these classes have library dependencies required for the classes to be loaded, they can be added via the codegenClasspath
configuration:
dependencies {
codegenClasspath("com.outr:scribe_3:3.15.0")
}
To assist with the complexity of interoperability between Kotlin and Scala's type systems, the task also has configurations for remapping type variables and conversion of parameter types.
For example, scribe
has a StringContext
extension function called formatter
which accepts varargs of Any
. This parameter becomes a Seq
when compiled which is cumbersome to use from Kotlin. The generated extension would be:
public fun StringContext.formatter(args: Seq<Any>): Formatter =
`package`.FormatterInterpolator.`formatter$extension`(this, args)
Seq
is not idiomatic in Kotlin, and we would prefer to use some Kotlin or Java collection instead.
We can remap this to an Array
instead by setting the parameterMappings
configuration in the task:
// replace scala Seq params with arrays
parameterMappings = listOf(
ParameterMapping(
originalType = Seq::class.asTypeName(),
newTypeName = Array::class.asTypeName(),
// use the MemberName in the template as opposed to a literal string so it gets imported automatically
templatePrefix = "%M(",
templatePrefixParams = listOf(CollectionConverters::class.member("asScala")),
templateSuffix = ".iterator()).toSeq()",
),
)
The templates follow the KotlinPoet template format.
This can be broken down as follows:
originalType
is the type to be replaced, hereSeq
newTypeName
is the new type to replace it with, hereArray
templatePrefix
is the KotlinPoet template to use ahead of anySeq
method parameter in the wrapper function (a prefix to theargs
parameter in our example)templatePrefixParams
is a list of parameters to pass to the prefix KotlinPoet template. We use the%M
and this parameter to ensure theasScala
method is imported.templateSuffix
is the KotlinPoet template to use after anySeq
method parameter in the wrapper function (a suffix to theargs
parameter in our example)templateSuffixParams
is a list of parameters to pass to the suffix KotlinPoet template. We have no parameters to pass to the template here, so it's been omitted.
Finally, the codegen library will automatically use varargs
for the last Array parameter in the generated extension function.
This will generate the following extension function:
import scala.jdk.javaapi.CollectionConverters.asScala
// ...
public fun StringContext.formatter(vararg args: Any): Formatter =
`package`.FormatterInterpolator.`formatter$extension`(this, asScala(args.iterator()).toSeq())
...and so we've substituted the args: Seq<Any>
parameter with vararg args: Any
from mapping the type, and args
with asScala(args.iterator()).toSeq()
when calling the wrapped function via our templates, improving the ergonomics of using the generated extension function.
See also example
Please read CONTRIBUTING.md for details on contributing and our code of conduct.