Skip to content

Usage Overview

kBars is one library, split into three modules by how you get your context data in:

graph TD
  core["kbars<br/>(parser, renderer, transforms; KpaStruct context)"]
  kxs["kbars-kxs<br/>(kotlinx.serialization JSON)"]
  yamlkt["kbars-yamlkt<br/>(YamlKt YAML)"]

  kxs --> core
  yamlkt --> core
  • You already have a KpaStruct context (perhaps built by hand, or via kPointer adapters for a format not listed below) → use kbars directly.
  • You have JSON text → use kbars-kxs.
  • You have YAML text → use kbars-yamlkt.

Both format modules re-export kbars as a transitive api dependency, so declaring the one format artifact is enough.

Adding a dependency

kBars is published on Maven Central under the com.commonsware.kbars group.

[versions]
kbars = "0.1.0"

[libraries]
kbars-kxs = { module = "com.commonsware.kbars:kbars-kxs", version.ref = "kbars" }
[versions]
kbars = "0.1.0"

[libraries]
kbars-yamlkt = { module = "com.commonsware.kbars:kbars-yamlkt", version.ref = "kbars" }
[versions]
kbars = "0.1.0"

[libraries]
kbars = { module = "com.commonsware.kbars:kbars", version.ref = "kbars" }

Then reference it from your module:

dependencies {
    implementation(libs.kbars.kxs) // or .yamlkt / .kbars
}

Rendering, in three steps

Whichever module you use, the shape is the same: build an environment, compile a template, render.

// 1. Build an environment from your data
val env = kbEnvironment.from("""{"name":"Alice"}""") // kbars-kxs

// 2. Compile a template
val template = kbTemplate.create("Hello {{ /name }}!")

// 3. Render — templates are reusable across renders (not thread-safe; reuse sequentially)
println(template.render(env)) // "Hello Alice!"

kbEnvironment holds the render context (your data), the registered transforms, the registered @env properties, and an optional partial loader. kbTemplate is a compiled, reusable template: it parses once, caching its AST and identifier classification, so rendering the same template many times — each with a distinct kbEnvironment — re-parses nothing after the first render.

Building directly against a KpaStruct (no JSON/YAML parsing) looks like this:

import com.commonsware.kpointer.adapter.KpaStruct

val context: KpaStruct = /* your context */
val env = kbEnvironment.create(context)
val template = kbTemplate.create("Hello {{ /name }}!")

println(template.render(env)) // "Hello Alice!"

Threading, lifecycle, and cost model

The rest of this section is the short version of the questions that come up first in server-side use: how long to keep a KbTemplate around, whether it's safe to share, and what actually costs time.

Compile once, render many. KbTemplate.create doesn't parse anything — parsing is deferred to the first render call, and the resulting AST (plus per-identifier classification and compiled-pointer caches) is retained on the KbTemplate instance afterward. A KbTemplate is meant to be built once per distinct template source — at startup, or the first time a given CMS-authored template is used — and reused for every subsequent render of that same source, each with its own KbEnvironment. Re-create-ing a KbTemplate from the same source string on every request throws away that caching for no benefit.

A KbTemplate is not thread-safe; reuse must be sequential. One render call must complete before the next begins on the same instance — concurrent renders from multiple threads against a single KbTemplate are not supported. If your server renders concurrently, either give each thread/coroutine its own KbTemplate instance (cheap relative to a fresh parse, since parsing happens once regardless, but still a separate cache) or serialize access to a shared instance; there is no internal locking to fall back on.

A KbEnvironment is cheap and disposable — build a fresh one per render. Unlike KbTemplate, KbEnvironment.create/fromJson/fromYaml are meant to be called once per render: they carry the render-specific context data, so sharing one across concurrent renders would mean sharing that data too. The registered transforms and @env properties are configured once per environment build (via the configure lambda) — there's no separate "compile" step to amortize the way there is for a template's parse.

Where the cost actually goes: parsing and per-identifier classification (once per distinct template source, amortized across every render); building an environment (proportional to how much you register in configure, done once per render); and the render itself (proportional to output size and how much of the context the template walks — {{#each}} over a large list has no engine-imposed cap, so a template's cost scales with the data it's handed). See Rendering Untrusted Content for the resource-exhaustion angle on that last point if the template source or context size is untrusted.

Where to next

  • Data Binding — feeding JSON, YAML, or a @Serializable class into kbars-kxs / kbars-yamlkt, and when to pick each.
  • Custom Transforms — writing and registering a KbTransform.
  • Custom Partials — registering a KbPartialLoader.
  • Environment Properties — registering @env properties and configuring the now clock.
  • Error Handling — the exception surface for app authors, and turning a KbRenderException into a good user-facing message.
  • Rendering Untrusted Content — recursion limits, output-size bounds, and other properties relevant when data or template source come from outside your app.
  • Writing Templates — the template-authoring side of kBars, if you're also the one writing the templates.