Skip to content

Commit 4450a49

Browse files
committed
feat: builder dsl configuration support for EncodedFormat and PackedFormat
1 parent 4661224 commit 4450a49

4 files changed

Lines changed: 120 additions & 50 deletions

File tree

src/main/kotlin/com/eignex/kencode/EncodedFormat.kt

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ package com.eignex.kencode
22

33
import PackedFormat
44
import kotlinx.serialization.*
5-
import kotlinx.serialization.modules.EmptySerializersModule
65
import kotlinx.serialization.modules.SerializersModule
76

7+
data class EncodedConfiguration(
8+
val codec: ByteEncoding = Base62,
9+
val checksum: Checksum? = null,
10+
val binaryFormat: BinaryFormat = PackedFormat.Default
11+
)
12+
813
/**
914
* Text `StringFormat` that combines:
1015
*
@@ -25,30 +30,34 @@ import kotlinx.serialization.modules.SerializersModule
2530
*/
2631
@OptIn(ExperimentalSerializationApi::class)
2732
open class EncodedFormat(
28-
val codec: ByteEncoding = Base62,
29-
val checksum: Checksum? = null,
30-
val binaryFormat: BinaryFormat = PackedFormat
33+
val configuration: EncodedConfiguration,
3134
) : StringFormat {
3235

36+
constructor(
37+
codec: ByteEncoding = Base62,
38+
checksum: Checksum? = null,
39+
binaryFormat: BinaryFormat = PackedFormat,
40+
) : this(EncodedConfiguration(codec, checksum, binaryFormat))
41+
42+
override val serializersModule: SerializersModule get() = configuration.binaryFormat.serializersModule
43+
3344
/**
3445
* Default format: `PackedFormat` + `Base62` without checksum.
3546
*/
3647
companion object Default : EncodedFormat()
3748

38-
override val serializersModule: SerializersModule = EmptySerializersModule()
39-
4049
/**
4150
* Serializes [value] with [binaryFormat], optionally appends [checksum],
4251
* and encodes the result using [codec].
4352
*/
4453
override fun <T> encodeToString(
4554
serializer: SerializationStrategy<T>, value: T
4655
): String {
47-
val bytes = binaryFormat.encodeToByteArray(serializer, value)
48-
val checked = if (checksum != null) {
49-
bytes + checksum.digest(bytes)
56+
val bytes = configuration.binaryFormat.encodeToByteArray(serializer, value)
57+
val checked = if (configuration.checksum != null) {
58+
bytes + configuration.checksum.digest(bytes)
5059
} else bytes
51-
return codec.encode(checked)
60+
return configuration.codec.encode(checked)
5261
}
5362

5463
/**
@@ -60,18 +69,40 @@ open class EncodedFormat(
6069
override fun <T> decodeFromString(
6170
deserializer: DeserializationStrategy<T>, string: String
6271
): T {
63-
val input = codec.decode(string)
64-
val bytes = if (checksum != null) {
65-
require(input.size >= checksum.size)
66-
val bytes = input.sliceArray(0..<input.size - checksum.size)
72+
val input = configuration.codec.decode(string)
73+
val bytes = if (configuration.checksum != null) {
74+
require(input.size >= configuration.checksum.size)
75+
val bytes = input.sliceArray(0..<input.size - configuration.checksum.size)
6776
val actual =
68-
input.sliceArray(input.size - checksum.size..<input.size)
69-
val expected = checksum.digest(bytes)
77+
input.sliceArray(input.size - configuration.checksum.size..<input.size)
78+
val expected = configuration.checksum.digest(bytes)
7079
require(actual.contentEquals(expected)) {
7180
"Checksum mismatch."
7281
}
7382
bytes
7483
} else input
75-
return binaryFormat.decodeFromByteArray(deserializer, bytes)
84+
return configuration.binaryFormat.decodeFromByteArray(deserializer, bytes)
7685
}
7786
}
87+
88+
class EncodedFormatBuilder {
89+
var codec: ByteEncoding = Base62
90+
var checksum: Checksum? = null
91+
var binaryFormat: BinaryFormat = PackedFormat.Default
92+
}
93+
94+
fun EncodedFormat(
95+
from: EncodedFormat = EncodedFormat.Default,
96+
builderAction: EncodedFormatBuilder.() -> Unit
97+
): EncodedFormat {
98+
val builder = EncodedFormatBuilder().apply {
99+
codec = from.configuration.codec
100+
checksum = from.configuration.checksum
101+
binaryFormat = from.configuration.binaryFormat
102+
}
103+
builder.builderAction()
104+
105+
val newConfig = EncodedConfiguration(builder.codec, builder.checksum, builder.binaryFormat)
106+
107+
return EncodedFormat(newConfig)
108+
}

src/main/kotlin/com/eignex/kencode/PackedDecoder.kt

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.eignex.kencode
22

3+
import PackedConfiguration
34
import kotlinx.serialization.DeserializationStrategy
45
import kotlinx.serialization.ExperimentalSerializationApi
56
import kotlinx.serialization.descriptors.*
@@ -8,12 +9,13 @@ import kotlinx.serialization.encoding.Decoder
89
import kotlinx.serialization.modules.EmptySerializersModule
910
import kotlinx.serialization.modules.SerializersModule
1011

12+
@Suppress("UNCHECKED_CAST")
1113
@OptIn(ExperimentalSerializationApi::class)
1214
class PackedDecoder(
13-
private val input: ByteArray
14-
) : Decoder, CompositeDecoder {
15-
15+
private val input: ByteArray,
16+
private val config: PackedConfiguration = PackedConfiguration(),
1617
override val serializersModule: SerializersModule = EmptySerializersModule()
18+
) : Decoder, CompositeDecoder {
1719

1820
internal var position: Int = 0
1921

@@ -199,10 +201,13 @@ class PackedDecoder(
199201

200202
override fun decodeIntElement(descriptor: SerialDescriptor, index: Int): Int {
201203
val anns = descriptor.getElementAnnotations(index)
202-
val zigZag = anns.hasVarInt()
203-
val varInt = anns.hasVarUInt() || zigZag
204+
val hasVarInt = anns.hasVarInt()
205+
val hasVarUInt = anns.hasVarUInt()
204206

205-
return if (varInt) {
207+
val zigZag = hasVarInt || (config.defaultZigZag && !hasVarUInt)
208+
val isVar = hasVarUInt || zigZag || config.defaultVarInt
209+
210+
return if (isVar) {
206211
val (raw, bytesRead) = PackedUtils.decodeVarInt(input, position)
207212
position += bytesRead
208213
if (zigZag) PackedUtils.zigZagDecodeInt(raw) else raw
@@ -213,10 +218,13 @@ class PackedDecoder(
213218

214219
override fun decodeLongElement(descriptor: SerialDescriptor, index: Int): Long {
215220
val anns = descriptor.getElementAnnotations(index)
216-
val zigZag = anns.hasVarInt()
217-
val varInt = anns.hasVarUInt() || zigZag
221+
val hasVarInt = anns.hasVarInt()
222+
val hasVarUInt = anns.hasVarUInt()
223+
224+
val zigZag = hasVarInt || (config.defaultZigZag && !hasVarUInt)
225+
val isVar = hasVarUInt || zigZag || config.defaultVarInt
218226

219-
return if (varInt) {
227+
return if (isVar) {
220228
val (raw, bytesRead) = PackedUtils.decodeVarLong(input, position)
221229
position += bytesRead
222230
if (zigZag) PackedUtils.zigZagDecodeLong(raw) else raw
@@ -248,12 +256,8 @@ class PackedDecoder(
248256
val kind = deserializer.descriptor.kind
249257
val isInline = deserializer.descriptor.isInline
250258

251-
// [CRITICAL FIX]
252-
// 1. Isolate complex structures (Classes, Lists, Maps) to separate their state.
253-
// 2. EXCLUDE Inline Classes (Value Classes) via `!isInline`. They are wrappers
254-
// and must share the parent's context to access field annotations (like @VarUInt).
255259
if ((kind is StructureKind.CLASS || kind is StructureKind.OBJECT || kind is StructureKind.LIST || kind is StructureKind.MAP || kind is PolymorphicKind) && !isInline) {
256-
val subDecoder = PackedDecoder(input)
260+
val subDecoder = PackedDecoder(input, config, serializersModule)
257261
subDecoder.position = this.position
258262
val value = deserializer.deserialize(subDecoder)
259263
this.position = subDecoder.position

src/main/kotlin/com/eignex/kencode/PackedEncoder.kt

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.eignex.kencode
22

3+
import PackedConfiguration
34
import kotlinx.serialization.ExperimentalSerializationApi
45
import kotlinx.serialization.SerializationStrategy
56
import kotlinx.serialization.descriptors.*
@@ -11,10 +12,10 @@ import java.io.ByteArrayOutputStream
1112

1213
@OptIn(ExperimentalSerializationApi::class)
1314
class PackedEncoder(
14-
private val output: ByteArrayOutputStream
15-
) : Encoder, CompositeEncoder {
16-
15+
private val output: ByteArrayOutputStream,
16+
private val config: PackedConfiguration = PackedConfiguration(),
1717
override val serializersModule: SerializersModule = EmptySerializersModule()
18+
) : Encoder, CompositeEncoder {
1819

1920
private var inStructure: Boolean = false
2021
private var isCollection: Boolean = false
@@ -32,9 +33,7 @@ class PackedEncoder(
3233

3334
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
3435
if (inStructure) {
35-
// [CRITICAL] Always spawn a new encoder for nested structures (Objects/Lists/Maps)
36-
// to prevent state pollution (bitmasks, isCollection flags) between levels.
37-
val childEncoder = PackedEncoder(dataBuffer)
36+
val childEncoder = PackedEncoder(dataBuffer, config, serializersModule)
3837
childEncoder.initializeStructure(descriptor)
3938
return childEncoder
4039
}
@@ -51,7 +50,7 @@ class PackedEncoder(
5150
PackedUtils.writeVarInt(collectionSize, target)
5251

5352
// Spawn new encoder for the collection items
54-
val childEncoder = PackedEncoder(target)
53+
val childEncoder = PackedEncoder(target, config, serializersModule)
5554
childEncoder.initializeStructure(descriptor)
5655
return childEncoder
5756
}
@@ -87,7 +86,6 @@ class PackedEncoder(
8786
dataBuffer.reset()
8887
}
8988

90-
// Helper to write to the correct buffer
9189
private fun getBuffer(): ByteArrayOutputStream = if (inStructure) dataBuffer else output
9290

9391
override fun encodeBoolean(value: Boolean) {
@@ -226,12 +224,14 @@ class PackedEncoder(
226224
}
227225

228226
override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) {
229-
// Only classes support per-field annotations
230227
val anns = descriptor.getElementAnnotations(index)
231-
val zigZag = anns.hasVarInt()
232-
val varInt = anns.hasVarUInt() || zigZag
228+
val hasVarInt = anns.hasVarInt()
229+
val hasVarUInt = anns.hasVarUInt()
233230

234-
if (varInt) {
231+
val zigZag = hasVarInt || (config.defaultZigZag && !hasVarUInt)
232+
val isVar = hasVarUInt || zigZag || config.defaultVarInt
233+
234+
if (isVar) {
235235
val v = if (zigZag) PackedUtils.zigZagEncodeInt(value) else value
236236
PackedUtils.writeVarInt(v, dataBuffer)
237237
} else {
@@ -241,10 +241,13 @@ class PackedEncoder(
241241

242242
override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) {
243243
val anns = descriptor.getElementAnnotations(index)
244-
val zigZag = anns.hasVarInt()
245-
val varInt = anns.hasVarUInt() || zigZag
244+
val hasVarInt = anns.hasVarInt()
245+
val hasVarUInt = anns.hasVarUInt()
246+
247+
val zigZag = hasVarInt || (config.defaultZigZag && !hasVarUInt)
248+
val isVar = hasVarUInt || zigZag || config.defaultVarInt
246249

247-
if (varInt) {
250+
if (isVar) {
248251
val v = if (zigZag) PackedUtils.zigZagEncodeLong(value) else value
249252
PackedUtils.writeVarLong(v, dataBuffer)
250253
} else {

src/main/kotlin/com/eignex/kencode/PackedFormat.kt

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import kotlinx.serialization.modules.EmptySerializersModule
55
import kotlinx.serialization.modules.SerializersModule
66
import java.io.ByteArrayOutputStream
77

8+
data class PackedConfiguration(
9+
val defaultVarInt: Boolean = false,
10+
val defaultZigZag: Boolean = false
11+
)
12+
813
/**
914
* Compact `BinaryFormat` optimized for small, flat Kotlin data classes.
1015
*
@@ -20,9 +25,12 @@ import java.io.ByteArrayOutputStream
2025
* For richer models, use `ProtoBuf` (or another [BinaryFormat]) with [com.eignex.kencode.EncodedFormat].
2126
*/
2227
@OptIn(ExperimentalSerializationApi::class)
23-
object PackedFormat : BinaryFormat {
24-
28+
open class PackedFormat(
29+
val configuration: PackedConfiguration = PackedConfiguration(),
2530
override val serializersModule: SerializersModule = EmptySerializersModule()
31+
) : BinaryFormat {
32+
33+
companion object Default : PackedFormat()
2634

2735
/**
2836
* Encodes [value] into a compact binary representation using [PackedEncoder].
@@ -31,7 +39,7 @@ object PackedFormat : BinaryFormat {
3139
serializer: SerializationStrategy<T>, value: T
3240
): ByteArray {
3341
val out = ByteArrayOutputStream()
34-
val encoder = PackedEncoder(out)
42+
val encoder = PackedEncoder(out, configuration, serializersModule)
3543
encoder.encodeSerializableValue(serializer, value)
3644
return out.toByteArray()
3745
}
@@ -42,7 +50,31 @@ object PackedFormat : BinaryFormat {
4250
override fun <T> decodeFromByteArray(
4351
deserializer: DeserializationStrategy<T>, bytes: ByteArray
4452
): T {
45-
val decoder = PackedDecoder(bytes)
53+
val decoder = PackedDecoder(bytes, configuration, serializersModule)
4654
return decoder.decodeSerializableValue(deserializer)
4755
}
4856
}
57+
58+
59+
class PackedFormatBuilder {
60+
var serializersModule: SerializersModule = EmptySerializersModule()
61+
var defaultVarInt: Boolean = false
62+
var defaultZigZag: Boolean = false
63+
}
64+
65+
fun PackedFormat(
66+
from: PackedFormat = PackedFormat.Default,
67+
builderAction: PackedFormatBuilder.() -> Unit
68+
): PackedFormat {
69+
val builder = PackedFormatBuilder().apply {
70+
serializersModule = from.serializersModule
71+
defaultVarInt = from.configuration.defaultVarInt
72+
defaultZigZag = from.configuration.defaultZigZag
73+
}
74+
builder.builderAction()
75+
76+
return PackedFormat(
77+
configuration = PackedConfiguration(builder.defaultVarInt, builder.defaultZigZag),
78+
serializersModule = builder.serializersModule
79+
)
80+
}

0 commit comments

Comments
 (0)