Skip to content

Builder generator for Kotlin classes using KSP

License

Notifications You must be signed in to change notification settings

open-toast/ksp-builder-gen

Repository files navigation

Builder Generator

CircleCI Maven Central

This is a KSP processor that generates builders for Kotlin classes. It is intended to provide just enough functionality to help migrate away from Immutables and ditch KAPT annotation processing.

Usage

See the integration-tests subproject for a working setup.

Gradle Setup

You have to bring in the KSP plugin

plugins {
    id("com.google.devtools.ksp") version "1.6.10-1.0.2"
}

and add the processor as a ksp dependency and the annotations as an implementation dependency:

dependencies {
    implementation("com.toasttab.ksp.builder:ksp-builder-gen-annotations:${version}")
    ksp("com.toasttab.ksp.builder:ksp-builder-gen-processor:${version}")
}

Code Generation

Unlike Immutables, you start with a simple Kotlin data-ish class. A more precise definition of a data-ish class is a class whose primary constructor's parameters are all backed by public properties. To generate a builder for a class, annotate the class with @GenerateBuilder.

@GenerateBuilder
class User(
    val name: String,
    val email: String?
)

The generated code will look like this

public class UserBuilder() {
    private var name: String? = null
    private var email: String? = null

    public constructor(o: User) : this() {
        this.name = o.name
        this.email = o.email
    }

    public fun name(name: String): UserBuilder {
        this.name = name
        return this
    }

    public fun email(email: String?): UserBuilder {
        this.email = email
        return this
    }

    public fun build(): User = User(name!!, email)
}

Collections

For basic collections (Collection, List, Set, Map), convenience mutators will be generated. For example,

@GenerateBuilder
class Container(
    val map: Map<String, Long>,
    val list: List<String>
)

will yield

public class ContainerBuilder() {
    public putMap(k: String, v: Long): ContainerBuilder // { ... }
    public putAllMap(Map<String, Long> map): ContainerBuilder // { ... }
    public addList(o: String): ContainerBuilder // { ... }
    public addAllList(Iterable<String>): ContainerBuilder // { ... }
}

Builder name

The name of the builder class is "${className}Builder" by default. It is customizable via the name annotation attribute.

Defaults

Defaults are supported via a custom annotation. Unfortunately, the code generator does not have access to parameters' default values, but it knows whether a default exists. For the sake of consistency, if the @Default annotation is present, the property must also have a Kotlin default.

@GenerateBuilder
class User(
    @GenerateBuilder.Default("true")
    val active = true
)

The @Default annotation is not supported for collection properties. You can do really bad things if you put complex expressions into the annotation; so don't.

Deprecation

For callsites written in Kotlin, it is typically desirable to use the constructor directly instead of calling the builder. Generated builders can be marked @Deprecated via the deprecated attribute.

Migration from Immutables

  • Convert the Immutables interface spec to a concrete Kotlin class, add @GenerateBuilder, and adapt existing callsites to the new builder.
  • Add deprecated = true to the @GenerateBuilder annotation when all callsites are converted to Kotlin.
  • ?
  • Profit