Decorations

A decoration is a bytes-in/bytes-out transform applied to one field's already-CBOR- encoded value, sitting between a Document<T> and the storage layer underneath it. This is the mechanism behind FieldDecorator — see Field Decorators for how to attach one.

The interface

interface FieldDecorator {
    fun wrap(fieldName: String, bytes: ByteArray): ByteArray
    fun unwrap(fieldName: String, bytes: ByteArray): ByteArray
}

wrap runs on every write, immediately before the encoded bytes reach storage. unwrap runs on every read, immediately after the bytes come back from storage and before they're decoded. fieldName is the field's own name — "title" for Note::title — never a raw storage key, so a decorator can single out individual fields with if (fieldName == Note::title.name) ....

Why fold in opposite directions

When more than one decorator is configured, writes fold through the list left-to-right and reads fold right-to-left:

decorators.fold(bytes) { acc, decorator -> decorator.wrap(fieldName, acc) }        // write
decorators.foldRight(bytes) { decorator, acc -> decorator.unwrap(fieldName, acc) } // read

Think of each decorator as a layer wrapped around the bytes. The last decorator to wrap on write is the outermost layer, so it has to be the first one peeled off on read — reversing the list on the read side is what makes unwrap undo wrap correctly for any combination of decorators, not just a single one. This is also why order is a real design choice: listOf(Compress, Encrypt) compresses first and encrypts the compressed result, so decompression only ever has to run on something that decrypted cleanly — reversing the pair would try to compress already-encrypted, high-entropy bytes, which doesn't shrink them.

A sketch of encryption as a decoration

Encryption is a natural fit for this shape: wrap encrypts the plaintext bytes, unwrap decrypts them back. A minimal sketch:

class EncryptingDecorator(private val cipher: Cipher) : FieldDecorator {
    override fun wrap(fieldName: String, bytes: ByteArray): ByteArray =
        cipher.encrypt(bytes, associatedData = fieldName.encodeToByteArray())

    override fun unwrap(fieldName: String, bytes: ByteArray): ByteArray =
        cipher.decrypt(bytes, associatedData = fieldName.encodeToByteArray())
}

Passing fieldName as associated data binds each ciphertext to its own field, so a value copied from one field's storage key into another's fails to decrypt instead of silently succeeding with the wrong field's data. See Session & User State for a complete, working AES-GCM implementation.

Failure contract

If unwrap throws SerializationException, IllegalStateException, or IllegalArgumentException, it surfaces as DocumentDecodingException — see Error Handling.