> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gorules.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Kotlin Rules Engine

> Integrate GoRules into your Kotlin application.

Install the ZEN Engine and evaluate your first decision in Kotlin.

## Installation

```kotlin theme={null}
dependencies {
    implementation("io.gorules:zen-engine-kotlin:0.4.7")
}
```

## Basic usage

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.JsonBuffer
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val ruleJson = object {}.javaClass.getResourceAsStream("/rules/pricing.json")!!.readBytes()

    ZenEngine(null, null).use { engine ->
        val decision = engine.createDecision(JsonBuffer(ruleJson))

        val input = JsonBuffer("""
            {
                "customer": { "tier": "gold", "yearsActive": 3 },
                "order": { "subtotal": 150, "items": 5 }
            }
        """)

        val response = decision.evaluate(input, null)

        println(response.result)
        // => {"discount":0.15,"freeShipping":true}
    }
}
```

## Loader

The loader pattern enables dynamic decision loading from any storage backend. Implement the `ZenDecisionLoaderCallback` interface with a `suspend fun load(key: String): JsonBuffer?` method. Use `ConcurrentHashMap` to cache decisions for optimal performance.

### File system

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.runBlocking

val cache = ConcurrentHashMap<String, ByteArray>()

val loader = object : ZenDecisionLoaderCallback {
    override suspend fun load(key: String): JsonBuffer? {
        val bytes = cache.computeIfAbsent(key) {
            Files.readAllBytes(Path.of("./rules", key))
        }
        return JsonBuffer(bytes)
    }
}

fun main() = runBlocking {
    val engine = ZenEngine(loader, null)

    engine.use {
        val response = engine.evaluate("pricing.json", JsonBuffer("{}"), null)
        println(response.result)
    }
}
```

### AWS S3

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.GetObjectRequest
import software.amazon.awssdk.regions.Region
import java.util.zip.ZipInputStream
import java.io.ByteArrayInputStream
import kotlinx.coroutines.runBlocking

val s3 = S3Client.builder().region(Region.US_EAST_1).build()
val rules = mutableMapOf<String, ByteArray>()

fun main() = runBlocking {
    // Download and extract all decisions at startup
    val request = GetObjectRequest.builder()
        .bucket("my-rules-bucket")
        .key("decisions.zip")
        .build()
    val zipBytes = s3.getObjectAsBytes(request).asByteArray()

    ZipInputStream(ByteArrayInputStream(zipBytes)).use { zis ->
        var entry = zis.nextEntry
        while (entry != null) {
            if (!entry.isDirectory) {
                rules[entry.name] = zis.readAllBytes()
            }
            entry = zis.nextEntry
        }
    }

    val loader = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            return rules[key]?.let { JsonBuffer(it) }
        }
    }

    val engine = ZenEngine(loader, null)

    engine.use {
        val response = engine.evaluate("pricing.json", JsonBuffer("{}"), null)
        println(response.result)
    }
}
```

### Azure Blob Storage

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import com.azure.storage.blob.BlobServiceClientBuilder
import java.util.zip.ZipInputStream
import java.io.ByteArrayInputStream
import kotlinx.coroutines.runBlocking

val blobService = BlobServiceClientBuilder()
    .connectionString(System.getenv("AZURE_STORAGE_CONNECTION"))
    .buildClient()
val container = blobService.getBlobContainerClient("rules")
val rules = mutableMapOf<String, ByteArray>()

fun main() = runBlocking {
    // Download and extract all decisions at startup
    val blob = container.getBlobClient("decisions.zip")
    val zipBytes = blob.downloadContent().toBytes()

    ZipInputStream(ByteArrayInputStream(zipBytes)).use { zis ->
        var entry = zis.nextEntry
        while (entry != null) {
            if (!entry.isDirectory) {
                rules[entry.name] = zis.readAllBytes()
            }
            entry = zis.nextEntry
        }
    }

    val loader = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            return rules[key]?.let { JsonBuffer(it) }
        }
    }

    val engine = ZenEngine(loader, null)

    engine.use {
        val response = engine.evaluate("pricing.json", JsonBuffer("{}"), null)
        println(response.result)
    }
}
```

### Google Cloud Storage

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import com.google.cloud.storage.StorageOptions
import java.util.zip.ZipInputStream
import java.io.ByteArrayInputStream
import kotlinx.coroutines.runBlocking

val storage = StorageOptions.getDefaultInstance().service
val rules = mutableMapOf<String, ByteArray>()

fun main() = runBlocking {
    // Download and extract all decisions at startup
    val blob = storage.get("my-rules-bucket", "decisions.zip")
    val zipBytes = blob.getContent()

    ZipInputStream(ByteArrayInputStream(zipBytes)).use { zis ->
        var entry = zis.nextEntry
        while (entry != null) {
            if (!entry.isDirectory) {
                rules[entry.name] = zis.readAllBytes()
            }
            entry = zis.nextEntry
        }
    }

    val loader = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            return rules[key]?.let { JsonBuffer(it) }
        }
    }

    val engine = ZenEngine(loader, null)

    engine.use {
        val response = engine.evaluate("pricing.json", JsonBuffer("{}"), null)
        println(response.result)
    }
}
```

## Coroutines

Evaluation functions are `suspend` functions, integrating natively with Kotlin coroutines:

```kotlin theme={null}
import kotlinx.coroutines.*

coroutineScope {
    val results = inputs.map { input ->
        async {
            decision.evaluate(JsonBuffer(input), null)
        }
    }.awaitAll()

    results.forEach { println(it.result) }
}
```

## Error handling

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenException

try {
    val response = decision.evaluate(input, null)
    println(response.result)
} catch (e: ZenException) {
    println("Evaluation failed: ${e.message}")
}
```

## Tracing

Enable tracing to inspect decision execution:

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEvaluateOptions

val options = ZenEvaluateOptions(trace = true, maxDepth = null)

val response = decision.evaluate(input, options)

println(response.trace)
// Each node's input, output, and performance timing

println(response.performance)
// Total evaluation time
```

## Expression utilities

Evaluate ZEN expressions outside of a decision context:

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.evaluateExpression
import io.gorules.zen_engine.kotlin.evaluateUnaryExpression
import io.gorules.zen_engine.kotlin.JsonBuffer

// Standard expressions
val result = evaluateExpression("a + b", JsonBuffer("""{ "a": 5, "b": 3 }"""))
// => 8

val total = evaluateExpression("sum(items)", JsonBuffer("""{ "items": [1, 2, 3, 4] }"""))
// => 10

// Unary expressions (comparison against $)
val isValid = evaluateUnaryExpression(">= 5", JsonBuffer("""{ "$": 10 }"""))
// => true

val inList = evaluateUnaryExpression("'US', 'CA', 'MX'", JsonBuffer("""{ "$": "US" }"""))
// => true
```

## Performance note

<Note>
  The Kotlin bindings use JNA (Java Native Access) for interoperability with the native Rust engine. This introduces some overhead compared to native Rust or direct bindings. We plan to revisit this when the FFM (Foreign Function & Memory) API becomes more widely adopted.
</Note>

## Best practices

**Use `.use {}` for resource management.** `ZenEngine` implements `AutoCloseable` to release native resources.

```kotlin theme={null}
ZenEngine(null, null).use { engine ->
    // use engine
}
```

**Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations.

**Implement a loader for dynamic decisions.** The loader pattern centralizes decision loading logic and enables caching with `ConcurrentHashMap`.

**Leverage coroutines for parallel evaluation.** Use `async`/`awaitAll` to evaluate multiple decisions concurrently.
