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.