Custom Transforms¶
The 46 built-in transforms cover strings, math, lists, and date-times, but sooner or later a template needs something app-specific — a currency formatter, a lookup against your own enum, a domain-specific string tweak. Writing one is the most common way to extend kBars beyond what Dokka's generated API reference shows on its own; this page works through it end to end.
The KbTransform interface¶
A transform is a functional interface:
public fun interface KbTransform {
public fun KbTransformScope.transform(input: KbTransformable?, arguments: List<KbTransformable?>): KbTransformable?
}
inputis the result of the prior step in the pipe chain (nullif nothing precedes it, or if the prior step yielded nothing).argumentsare the transform's own:-separated arguments (see Arguments), already resolved to values — empty when the template didn't pass any.- The lambda runs with
KbTransformScopeas its receiver, which is where the coercion and construction helpers below come from. - The return value becomes the input to the next step in the chain (or the rendered value, if this is the last step).
Registering one¶
Register transforms through the same configure DSL lambda every KbEnvironment factory function
accepts — KbEnvironment.create, KbEnvironment.fromJson, KbEnvironment.fromYaml:
val env =
KbEnvironment.create(context) {
addTransform("reverse") { input, _ -> text(asString(input).reversed()) }
}
val template = KbTemplate.create("{{ name | reverse }}")
println(template.render(env)) // context { "name": "alice" } -> "ecila"
addTransform throws IllegalStateException if the name is already registered — including the 46
built-ins. Use overrideTransform instead when you're
deliberately replacing one.
Accepting arguments¶
Arguments arrive as List<KbTransformable?>, in template source order. Validate the count before
indexing into it:
addTransform("shout") { input, args ->
require(args.size <= 1) { "shout: takes at most 1 argument" }
val punctuation = if (args.isEmpty()) "!" else asString(args[0])
text(asString(input).uppercase() + punctuation)
}
Reading and producing values¶
input and each entry in arguments are KbTransformable?, not raw strings — the same wrapper type
that flows through the whole pipe chain. KbTransformScope supplies the coercion and construction
functions used to move between it and Kotlin types:
| Direction | Function | Notes |
|---|---|---|
Read as String |
asString(value) |
Same rule {{ }} interpolation uses; null reads as "null". |
Read as KpaElement? |
asElement(value) |
Unwraps to the underlying kPointer element (struct/list/primitive). |
Read as KpaList? |
asList(value) |
null if value doesn't wrap a list. |
Read as Int? |
asIntOrNull(value) |
Routes through asString then toIntOrNull. |
Read as Double? |
asNumberOrNull(value) |
null if value isn't numeric (or a numeric-looking string). |
Read as Instant? |
asDateTimeOrNull(value) |
null unless value is a KbTransformable.DateTime. |
| Produce a string result | text(value: String) |
|
| Produce a numeric result | number(value: Double) |
|
| Produce a boolean result | bool(value: Boolean) |
|
| Produce a list result | list(items: List<KpaElement>) |
|
| Produce a date-time result | dateTime(value: Instant) |
Renders as an ISO-8601 UTC string, like @env/now. |
| Wrap an existing element | element(value: KpaElement?) |
For when you already have a KpaElement from factory or elsewhere. |
factory (the active KpaElementFactory) is also available on the scope directly, for building
structs or lists from scratch rather than transforming an existing one — see the built-in
list transforms for examples that do this.
Error handling inside a transform¶
There are two ways for a transform to signal a problem, and the built-ins use both, for different kinds of problem:
Soft-fail for problems with the data. Call softFail(category, problem, input) and return its
result (always null) when the input or an argument is the wrong shape for what the transform
does — a non-numeric string passed to a math transform, an unparseable date string, and so on. This
is recoverable: the render continues, the expression evaluates to nothing, and (if the app registered
a KbEventListener) the failure is reported as a structured
KbSoftFailEvent instead of silently vanishing.
addTransform("half") { input, _ ->
val n = asNumberOrNull(input) ?: return@addTransform softFail(
KbSoftFailEvent.Category.TypeMismatch,
"half: non-numeric input",
input,
)
number(n / 2)
}
Throw for problems with the template itself. An argument-count mismatch, or any other error a
template author could only fix by editing the template, is not data-dependent — it's the same every
time this template renders. The built-in transforms throw IllegalStateException (typically via
require/check) for exactly this case, and kBars's renderer catches IllegalArgumentException and
IllegalStateException raised from inside a transform, attaches the failing expression's source
location, and re-throws it as a KbRenderException from KbTemplate.render.
Your custom transforms get the same treatment for free:
addTransform("nth") { input, args ->
require(args.size == 1) { "nth: requires exactly 1 argument, got ${args.size}" }
// ...
}
try {
template.render(env)
} catch (e: KbRenderException) {
// e.message includes "nth: requires exactly 1 argument, got 0" plus the template location
}
Any other exception type thrown from inside a transform propagates unwrapped — reserve that for genuine bugs in the transform's own implementation, not for expected input or template-author mistakes.
Overriding and removing built-ins¶
The same builder methods that add a new transform can replace or remove an existing one — including any of the 46 built-ins:
KbEnvironment.create(context) {
overrideTransform("upcase") { input, _ -> text(asString(input).uppercase().replace('İ', 'I')) }
removeTransform("downcase") // "downcase" is now an unknown-transform render error if referenced
}
overrideTransform never throws for an unknown name — it behaves like addTransform in that case.
removeTransform is a no-op if the name isn't registered.
Where to next¶
- Transforms — the
|pipeline syntax and the full built-in catalog. - Environment Properties — registering
@envproperties with the same builder DSL, and theKbEventListenerthat observes soft failures. - API Reference — the full Dokka reference for
KbTransform,KbTransformScope, andKbTransformable.