An open API service indexing awesome lists of open source software.

https://github.com/schleifenkauz/krobot

Enhanced version of kotlin-robot
https://github.com/schleifenkauz/krobot

Last synced: 6 months ago
JSON representation

Enhanced version of kotlin-robot

Awesome Lists containing this project

README

          

# KRobot - DSL-based Kotlin code generation

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...

````kotlin
kotlinFile {
`package`("com.example")
internal.`object`("Main").body {
`@`("JvmStatic").`fun`("main", "args" of "Array").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`.

```kotlin
package com.example

internal object Main {
@JvmStatic
fun main(args: Array) {
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"
}
}
}
```

## Using the library

KRobot is available from Maven Central.

### Gradle

```groovy
dependencies {
implementation 'com.github.nkb03:krobot:1.0'
}
```

### Maven

```xml

com.github.nkb03
krobot
1.0

```

## Introductory tutorial

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:
```kotlin
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
```kotlin
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.
```kotlin
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().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:
````kotlin
package foo.bar
import kotlin.random.Random
import java.awt.Robot
abstract class ExampleClass @PublishedApi internal constructor(private val wrapped: List): List by wrapped, Any() {
inline fun f(x: Int = 3, l: List, 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.

### Templates

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 `@`, where `idx` is the (one-based) index of the referenced parameter.
For example, `"val @1 = 1".format("x")` evaluates to `"val x = 1"`.
If this parameter is `null`, 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 other `Iterable`s).
The syntax is `*{}`.
For example, `"val x = listOf(*1{, })".format(listOf(lit(1), lit(2), lit(3)))` evaluates to `val x = listOf(1, 2, 3)"`.
If the separator is only one character long, the curly brackets can be omitted.
If the referenced parameter is `null`, 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 `*{}[.]`.
Appending the transformation has the effect of substituting the individual elements of the referenced list
for the interpolations referencing `` into the ``.
The functionality can be compared to the `map`-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:
````kotlin
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())
}
````
It generates the following code:
````kotlin
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
````

## Contributing

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.

### Development setup

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.

## Author

Nikolaus Knop (niko.knop003@gmail.com)

## License

See [LICENSE.MD](LICENSE.md)