Skip to content

Repository files navigation

Banner

Maven Central

JVM JS Wasm Linux macOS iOS Windows


Amber is a lightweight, multiplatform, compile-time, reflectionless collection of utilities that promote immutability and readability in Kotlin code.

The library was developed out of necessity for the Quarkdown typesetting system, and is extensively adopted there.

Table of contents

Installation

plugins {
    id("com.quarkdown.amber") version "3.0.1"
}

repositories {
    mavenCentral()
}

Also make sure Maven Central is enabled as a plugin repository in your settings.gradle.kts:

pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}

 

Features

 

Deep-copying data classes

Annotating a data class with @NestedData will provide a deepCopy function, allowing waterfall copying of nested data classes.

The real power of this function is the flattening of nested properties:

import com.quarkdown.amber.annotations.NestedData

@NestedData
data class Config(
    val app: AppConfig,
    val notifications: NotificationConfig,
)

data class AppConfig(
    val theme: String,
)

data class NotificationConfig(
    val email: EmailNotificationConfig,
    val push: PushNotificationConfig,
)

data class EmailNotificationConfig(
    val enabled: Boolean,
    val frequency: String,
)

Without the library, generating a copy with a modified nested property is a verbose operation:

val newConfig: Config = config.copy(
    app = config.app.copy(theme = "dark"),
    notifications = config.notifications.copy(
        email = config.notifications.email.copy(enabled = false)
    )
)

With deepCopy, it becomes much more concise:

val newConfig: Config = config.deepCopy(
    appTheme = "dark",
    notificationsEmailEnabled = false,
)

 

Merging data classes

Annotating a data class with @Mergeable will provide a merge function.

@Mergeable
data class MyClass(
    val a: String,
    val b: Int? = null,
    val c: Boolean? = null,
)

val first = MyClass(a = "X", b = 42)
val second = MyClass(a = "Y", b = 7, c = true)

val merged: MyClass = first.merge(second) // MyClass(a=X, b=42, c=true)

Real-world example

The library's main purpose is to abstract away from rigid defaults, making it possible to create flexible configurations.

import com.quarkdown.amber.annotations.Mergeable

@Mergeable
data class Preferences(
    val theme: String? = null, // If null, use system default
    val fontSize: Int? = null, // If null, use system default
    val autoSaveDelay: Int? = null, // If null, disable auto-save
)

object DefaultPreferencesFactory {
    fun mobile() = Preferences(theme = "light")
    fun desktop() = Preferences(fontSize = 16, autoSaveDelay = 30)
}

fun main() {
    val default = DefaultPreferencesFactory.desktop()
  
    // Assume user preferences are loaded from a config file.
    val user = Preferences(theme = "dark", autoSaveDelay = 10)

    // Merging user preferences with defaults. User values take precedence.
    val preferences: Preferences = user.merge(default)
    println(preferences) // Preferences(theme=dark, fontSize=16, autoSaveDelay=10)
}

 

Exporting resources

Annotating a class or object with @ExportResource will read the given resource at compile time and expose its content as a String property. The text is inlined in the generated code, so that no file or classloader access happens at runtime.

import com.quarkdown.amber.annotations.ExportResource

@ExportResource("/templates/page.html")
@ExportResource("/templates/theme.css", name = "style")
object Templates

fun main() {
    println(Templates.page)  // Content of /templates/page.html
    println(Templates.style) // Content of /templates/theme.css
}

 

Diverging classes

Annotating a class or its constructor parameters with @Diverge will provide a diverge function, similar to a data class's copy, but available on non-data classes and exposing only the marked parameters.

This is useful when you need a copy-like function but can't make the class a full data class.

import com.quarkdown.amber.annotations.Diverge

class Person(
    val name: String,
    @Diverge val age: Int,
    @Diverge val city: String,
)

val person = Person("Alice", 30, "New York")
val p1 = person.diverge(age = 31)                  // Person(name=Alice, age=31, city=New York)
val p2 = person.diverge(age = 31, city = "Boston") // Person(name=Alice, age=31, city=Boston)
val p3 = person.diverge(name = "Bob")              // Error: 'name' is not marked with @Diverge

 

Troubleshooting

Make sure that annotation processing is enabled in your IDE to ensure the generated functions can be resolved. Alternatively, build the project right after marking a class with an annotation.

About

🏵️ Compile-time utilities for Kotlin, written in stone.

Topics

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages