Writing a Custom Adapter¶
The kpointer-adapter module decouples pointer navigation and mutation from any concrete tree
representation. The built-in JSON, YAML, and XML/HTML modules
are all thin adapters over this API. If you have a different tree type — another JSON library, a
config model, an in-memory DOM — you can teach kPointer about it by implementing the same
interfaces.
Most consumers never depend on kpointer-adapter directly. Reach for it only when you are
implementing an adapter for a new tree type, or writing generic code that must work across
adapter families.
The interfaces¶
Every addressable value is a KpaElement, a sealed interface with exactly three implementors:
| Interface | Role |
|---|---|
KpaStruct |
Object-shaped node addressed by string keys. |
KpaList |
Array-shaped node addressed by integer indices. |
KpaPrimitive |
Leaf value: string, number, boolean, or null. |
KpaElementFactory |
Per-family factory that wraps primitives and rebuilds structs/lists. |
Reading works on the read-only interfaces above. Mutation requires two more:
| Interface | Adds |
|---|---|
RebuildableKpaStruct : KpaStruct |
a factory: KpaElementFactory property |
RebuildableKpaList : KpaList |
a factory: KpaElementFactory property |
The mutate { … } DSL extensions are defined only on the Rebuildable* interfaces. That is a
deliberate design choice: a read-only adapter (say, one over an immutable DOM you cannot rebuild)
implements plain KpaStruct / KpaList, and calling .mutate {} on it is a compile error
rather than a runtime surprise.
graph TD
element["KpaElement (sealed)"]
struct["KpaStruct"]
list["KpaList"]
primitive["KpaPrimitive"]
rstruct["RebuildableKpaStruct"]
rlist["RebuildableKpaList"]
element --> struct
element --> list
element --> primitive
struct --> rstruct
list --> rlist
Deciding: read-only or mutable?¶
- If your tree can be rebuilt from a snapshot (a new map/list produces a new node), implement the
Rebuildable*interfaces so consumers getmutate. - If your tree is read-only or cannot be reconstructed, implement plain
KpaStruct/KpaListand skip the factory. Consumers can still read by pointer.
A read-only adapter¶
The minimum for reads is KpaStruct, KpaList, and KpaPrimitive:
class MyStruct(private val backing: MyObject) : KpaStruct {
override fun get(key: String): KpaElement? = backing[key]?.toKpaElement()
override fun contains(key: String): Boolean = key in backing
override val keys: Set<String> get() = backing.keys()
override fun toMap(): Map<String, KpaElement> =
backing.entries().associate { it.key to it.value.toKpaElement() }
}
class MyList(private val backing: MyArray) : KpaList {
override fun get(index: Int): KpaElement = backing[index].toKpaElement()
override val size: Int get() = backing.size
override fun toList(): List<KpaElement> = backing.map { it.toKpaElement() }
}
toKpaElement() here is your own conversion extension that wraps a native node in the right
KpaElement. See Wrapping the backing value for a shortcut.
get semantics matter for conformance
KpaStruct.get must return null for an absent key, and KpaList.get is allowed to throw
for an out-of-range index (the pointer resolver treats that as absent). Returning a non-null
"empty" placeholder, or throwing for a missing struct key, breaks the
absent-vs-error rule that the conformance suite
enforces.
A mutable adapter¶
Add the factory and implement RebuildableKpaStruct / RebuildableKpaList, plus a
KpaElementFactory that can wrap primitives and rebuild containers:
object MyFactory : KpaElementFactory {
override fun String.toPrimitive(): KpaPrimitive = MyPrimitive(MyString(this))
override fun Boolean.toPrimitive(): KpaPrimitive = MyPrimitive(MyBool(this))
override fun Int.toPrimitive(): KpaPrimitive = MyPrimitive(MyNum(this))
override fun Long.toPrimitive(): KpaPrimitive = MyPrimitive(MyNum(this))
override fun Float.toPrimitive(): KpaPrimitive = MyPrimitive(MyNum(this))
override fun Double.toPrimitive(): KpaPrimitive = MyPrimitive(MyNum(this))
override val nullPrimitive: KpaPrimitive = MyPrimitive(MyNull)
override fun withMap(map: Map<String, KpaElement>): RebuildableKpaStruct =
MyStruct(rebuildObjectFrom(map))
override fun withList(list: List<KpaElement>): RebuildableKpaList =
MyList(rebuildArrayFrom(list))
}
class MyStruct(val backing: MyObject) : RebuildableKpaStruct {
// ...the four read members from above...
override val factory: KpaElementFactory get() = MyFactory
}
The mutate machinery threads factory from the root and calls withMap / withList to
reconstruct each changed container, so nested elements themselves need not be rebuildable. The result
of a mutation is a new tree of your adapter type; the original is never touched.
Reducing boilerplate¶
kpointer-adapter ships shared abstractions so you write only what is genuinely per-family.
Wrapping the backing value¶
Implement BackedKpaElement<B> to expose your native node through a single backing property. The
companion requireBacking<B>(libraryName) unwraps any KpaElement back to its native value (or
throws a descriptive error), so your toXxxElement() conversion can be a one-liner:
AbstractKpaList and AbstractKpaStructMap¶
These base classes delegate the read members to a backing List<E> / Map<String, E>, leaving you
to supply only wrapElement (and factory, if mutable):
class MyList(backing: List<MyNode>) : AbstractKpaList<MyNode>(backing) {
override fun wrapElement(element: MyNode): KpaElement = element.toKpaElement()
}
AbstractKpaStructMap is for native maps that already use String keys. If your map uses a richer
key type (as YamlKt does), implement KpaStruct directly and project to string keys yourself.
StringKpaPrimitive¶
For leaf types that are always strings (an XML attribute value, a text node), extend
StringKpaPrimitive and override the single content property — it fixes every type flag to the
string case for you:
Verifying your adapter¶
Because the adapter contract is exactly what the conformance suite exercises,
the surest test of a new adapter is to run the portable algorithm/resolve.json and
algorithm/mutate.json fixtures through it. The reference JSON adapter does precisely this — a green
run is evidence that your get, mutate, and absent-vs-error behavior all match the spec.