KRobot helps you with generating Kotlin files programmatically, for example in annotation processors. What differentiates KRobot from other existing Kotlin code generation libraries is, that it uses Kotlin's capabilities for building domain specific languages wherever possible. The API tries to add as little noise to your code as possible, so you can concentrate on what really matters. This can make meta-code look almost like the files that are being generated. For example the following...
kotlinFile {
`package`("com.example")
internal.`object`("Main").body {
`@`("JvmStatic").`fun`("main", "args" of "Array<String>").body {
`val`("msg") initializedWith `when`("args".e.select("size")) {
lit(0) then lit("This is not possible...")
lit(1) then lit("Oh no, you provided no arguments")
`in`(lit(2)..lit(5)) then lit("The number of arguments is ok")
`else` then lit("Too many arguments")
}
}
}
}.saveTo(File("example.kt"))
...writes this Kotlin code to the file example.kt
.
package com.example
internal object Main {
@JvmStatic
fun main(args: Array<String>) {
val msg = when (args.size) {
0 -> "This is not possible..."
1 -> "Oh no, you provided no arguments"
in 2..5 -> "The number of arguments is ok"
else -> "Too many arguments"
}
}
}
KRobot is available from Maven Central.
dependencies {
implementation 'com.github.nkb03:krobot:1.0'
}
<dependency>
<groupId>com.github.nkb03</groupId>
<artifactId>krobot</artifactId>
<version>1.0</version>
</dependency>
The main entry into the world of KRobot is the function kotlinFile
from the package krobot.api
.
(In general, all the declarations that are necessary for ordinary use of the library are located in the krobot.api
-package. The krobot.ast
-package contains the underlying data structures used by the API, which may be needed to be
explicitly accessed when extending the API in client code.)
Inside the closure, that is accepted by the kotlinFile
-function, you can add the declarations, you want to add to the
file. It then produces an instance of the KotlinFile
-class containing in AST-form all the declarations that were added
in the closure, which can be saved on the disk using on of the saveTo
-functions.
A small example:
kotlinFile {
`package`("crazy.maths")
`fun`("square", "x" of "Int") returnType "Int" returns "x".e * "x".e
}.saveTo(File("generatedMaths.kt"))
A useful utility are the saveToSourceRoot
-functions, which take the package declaration of a file into consideration.
For example
kotlinFile {
`package`("foo.bar.baz")
//declarations...
}.saveToSourceRoot(File("build/generated-src"), "foo.kt")
Puts the generated file foo.kt
into the directory build/generated/foo/bar/baz
.
Until I have written more documentation on the individual features, this fairly extensive example should serve the purpose of introducing you to the library.
import krobot.api.*
import java.io.File
fun main() {
kotlinFile {
import("kotlin.random.Random")
`package`("foo.bar")
+abstract.`class`("ExampleClass", `in`("T"))
.primaryConstructor(
`@`("PublishedApi").internal,
private.`val`.parameter("wrapped") of type("List", "Int")
)
.implements(type("List", "Int"), by = get("wrapped"))
.extends("Any", emptyList()) body {
+inline.`fun`(
listOf(invariant("T") lowerBound "Any"),
"f",
"x" of "Int" default lit(3),
"l" of type("List", "Int"),
crossinline.parameter("block") of import<java.awt.Robot>().functionType(
type("Int"),
returnType = type("Int")
)
) returnType "Int" body {
+call("println", get("x"))
+`if`(get("x") eq lit(3)).then {
+"println"(lit("default value supplied"))
+"println"(lit("test"))
}.`else` {
+"println"(lit("value of \$x supplied"))
+"println"(lit("test"))
}
+"require"(get("l").call("sum") + get("x") less lit(10), closure { +lit("error") })
+`when` {
get("x") eq lit(1) then {
+"println"(lit(1))
}
`else` {
+"println"(lit(2))
}
}
+`when`(get("x")) {
`is`("Int") then "println"(lit("is integer"))
`in`(`this`("ExampleClass")) then "println"(lit("is in collection"))
lit(3) then "println"(lit("is three"))
`else` {
+"println"(lit("hurray"))
}
}
}
+private.constructor("test" of "Int").delegate("listOf"("test".e, "Random".e.call("nextInt")))
+abstract.`fun`("f") returnType "Int"
+public.`class`("Inner")
+internal.enum("E").primaryConstructor(`val`.parameter("x") of "Int".t).body {
+abstract.`fun`("f") returnType "Int"
+"X"("1".e) {
+override.`fun`("f") returnType "Int" returns "1".e
}
}
}
}.saveTo(File("generated.kt"))
}
It generates the following Kotlin file:
package foo.bar
import kotlin.random.Random
import java.awt.Robot
abstract class ExampleClass<in T> @PublishedApi internal constructor(private val wrapped: List<Int>): List<Int> by wrapped, Any() {
inline fun <T: Any> f(x: Int = 3, l: List<Int>, crossinline block: Robot.(Int) -> Int): Int {
println(x)
if(x == 3) {
println("default value supplied")
println("test")
} else {
println("value of $x supplied")
println("test")
}
require(l.sum() + x < 10) { "error" }
when {
x == 1 -> println(1)
else -> println(2)
}
when(x) {
is Int -> println("is integer")
in this@ExampleClass -> println("is in collection")
3 -> println("is three")
else -> println("hurray")
}
}
private constructor(test: Int) : this(listOf(test, Random.nextInt()))
abstract fun f(): Int
public class Inner
internal enum class E (val x: Int) {
X(1){
override fun f(): Int = 1
};
abstract fun f(): Int
}
}
If you are unsure how to generate a specific language construct, you can create an Issue on GitHub.
In some cases, it can be easier to just use string interpolation instead of building an abstract syntax tree.
KRobot implements a simple templating language, that provides the basic functionality needed to generate code.
It can be arbitrarily mixed with the structured API.
The templating language has only a couple of concepts:
- The syntax for interpolation is
@<idx>
, whereidx
is the (one-based) index of the referenced parameter.
For example,"val @1 = 1".format("x")
evaluates to"val x = 1"
.
If this parameter isnull
, the form is replaced with an empty string.
For example,"println(@1)".format(null)
evaluates to"println()"
- The spread operator
*
works like the interpolation operator just with lists (and otherIterable
s). The syntax is*<idx>{<separator>}
.
For example,"val x = listOf(*1{, })".format(listOf(lit(1), lit(2), lit(3)))
evaluates toval x = listOf(1, 2, 3)"
.
If the separator is only one character long, the curly brackets can be omitted. If the referenced parameter isnull
, empty, or contains null-elements, the form is replaced by an empty string.
For example"listOf(*1{, })".format(listOf("1", null))
evaluates to"listOf()"
. - A spread form may be followed by a transformation.
The syntax is
*<idx>{<sep>}[<v>.<body>]
. Appending the transformation has the effect of substituting the individual elements of the referenced list for the interpolations referencing<v>
into the<body>
. The functionality can be compared to themap
-function from Kotlin.
An example:"listof(*1{, }[2.@2 + @2])".format(listOf(lit(1), lit(2), lit(3)))
evaluates to"listOf(1 + 1, 2 + 2, 3 + 3)".
- Groups enclosed by curly brackets are skipped (that is, replaced by an empty string),
if any parameter referenced inside the group is null,
if a list referenced by the spread operator inside the group is empty or contains null-elements,
or if a group enclosed in the outer group is skipped.
Some examples:"{val x = @1}".format(null)
evaluates to""
."val x = @1{ + @2}".format(lit(1), lit(2))
evaluates to"val x = 1"
."1 + 2 + 3{ + *2{ + }}".format(lit(4), lit(5), null)
evaluates to"1 + 2 + 3"
"val x = @2 {+ {@1 * @2} + {@1 * @3}}".format(lit(1), lit(2), null)
evaluates to"val x = 2"
.
- The characters
@
,*
,{
,}
,[
and]
can be escaped by putting a backslash before them.
For example,"(@1..@2).forEach \\{ println(it \\* it) \\}".format(lit(1), lit(5))
.
Template
s can be created using the function Template.parse(raw: String)
from the krobot.templates
-package
and instantiated with the function format(vararg arguments: Any?)
of the Template
-class.
You can also use the function String.format(vararg arguments: Any?)
which is defined as an extension
and creates a Template
under the hood.
The format
-functions return a TemplateElement
which can added as declarations or statements with the +
-operator
or used as types or expressions. See the following example:
val f = kotlinScript {
+"val x = 1"
+"val @1 = @2".format("y", lit(2) + lit(3))
+`fun`("f", "vararg xs" of "Int") returnType "Int" returns "xs".e.call("asList").call("sum()")
+"val f = 0"
val template = Template.parse("val @1 = f{(*2{, })}")
+template.format("a", listOf(lit(1), lit(2), lit(3)))
+template.format("b", emptyList<Expr>())
}
It generates the following code:
val x = 1
val y = 2 + 3
fun f(vararg xs: Int): Int = xs.asList().sum()()
val f = 0
val a = f(1, 2, 3)
val b = f
Contributions are greatly appreciated. You can contribute by...
- ...using it in your project, asking questions, reporting bugs, and suggesting features.
- ...implementing new features yourself and making a pull request.
- ...extending the test coverage.
- ...writing documentation for the API in the form of KDoc comments.
If you want to use the project or contribute and have questions, please feel free to get help from me - either via email or by creating an issue on GitHub.
Developers who wish to contribute to KRobot are advised to fork the repository and open the project in Intellij. If any difficulties arise during the project import, feel free to contact me.
Nikolaus Knop (niko.knop003@gmail.com)
See LICENSE.MD