diff --git a/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/ElementDeclarationContext.kt b/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/ElementDeclarationContext.kt index 69f1d34f..8d73b2bb 100644 --- a/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/ElementDeclarationContext.kt +++ b/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/ElementDeclarationContext.kt @@ -17,3 +17,7 @@ val ElementDeclarationContext.elementAnnotations: List inline fun ElementDeclarationContext.findElementAnnotation(): T? { return elementAnnotations.findAnnotation() } + +inline fun ElementDeclarationContext.hasAnnotation(): Boolean { + return findElementAnnotation() != null +} diff --git a/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/annotations/DisableOptionalBooleanOptimization.kt b/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/annotations/DisableOptionalBooleanOptimization.kt new file mode 100644 index 00000000..a653d200 --- /dev/null +++ b/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/annotations/DisableOptionalBooleanOptimization.kt @@ -0,0 +1,15 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.annotations + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialInfo + +/** + * Disables the SCALE `Option` single-byte optimization for the annotated `Boolean?` property. + * When present, the property is encoded using the standard 2-byte optional encoding: + * `0x00` for null, `0x01 0x00` for false, `0x01 0x01` for true. + */ +@SerialInfo +@Target(AnnotationTarget.PROPERTY) +annotation class DisableOptionalBooleanOptimization diff --git a/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/decoder/BaseCompositeBinaryDecoder.kt b/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/decoder/BaseCompositeBinaryDecoder.kt index a4984809..1493f83b 100644 --- a/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/decoder/BaseCompositeBinaryDecoder.kt +++ b/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/decoder/BaseCompositeBinaryDecoder.kt @@ -2,7 +2,9 @@ package io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.dec import io.emeraldpay.polkaj.scale.ScaleCodecReader import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.ElementDeclarationContext +import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.annotations.DisableOptionalBooleanOptimization import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.common.ScaleOptional.NULL_MARK +import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.findElementAnnotation import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.descriptors.SerialDescriptor @@ -70,7 +72,9 @@ abstract class BaseCompositeBinaryDecoder( if (nullabilityByte == NULL_MARK) return null val elementContext = ElementDeclarationContext(index, descriptor) - val delegate = PrimitiveBinaryScaleDecoder(serializersModule, reader, elementContext, nullabilityByte) + val disableOptBool = elementContext.findElementAnnotation() != null + val passedNullabilityByte = if (disableOptBool) null else nullabilityByte + val delegate = PrimitiveBinaryScaleDecoder(serializersModule, reader, elementContext, passedNullabilityByte) return delegate.decodeSerializableValue(deserializer) } diff --git a/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/encoder/CompositeBinaryEncoder.kt b/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/encoder/CompositeBinaryEncoder.kt index 14992c26..515a970f 100644 --- a/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/encoder/CompositeBinaryEncoder.kt +++ b/koltinx-serialization-scale/src/main/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/binary/encoder/CompositeBinaryEncoder.kt @@ -3,9 +3,12 @@ package io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.enc import io.emeraldpay.polkaj.scale.ScaleCodecWriter import io.novasama.substrate_sdk_android.extensions.toSignedBytes import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.ElementDeclarationContext +import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.annotations.DisableOptionalBooleanOptimization import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.common.ScaleOptional.NOT_NULL_MARK import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.common.ScaleOptional.NULL_MARK import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.common.encodeOptionalBoolean +import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.findElementAnnotation +import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.hasAnnotation import io.novasama.substrate_sdk_android.scale.utils.directWrite import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException @@ -130,7 +133,7 @@ internal class CompositeBinaryEncoder( ) { when { serializer.descriptor.kind == PrimitiveKind.BOOLEAN -> { - writer.encodeOptionalBoolean(value as Boolean?) + encodeOptionalBoolean(descriptor, index, value) } value == null -> { @@ -144,6 +147,26 @@ internal class CompositeBinaryEncoder( } } + private fun encodeOptionalBoolean( + descriptor: SerialDescriptor, + index: Int, + value: T? + ) { + val elementContext = ElementDeclarationContext(index, descriptor) + val optimizeBool = !elementContext.hasAnnotation() + + when { + optimizeBool -> writer.encodeOptionalBoolean(value as Boolean?) + + value == null -> writer.writeByte(NULL_MARK) + + else -> { + writer.writeByte(NOT_NULL_MARK) + ScaleCodecWriter.BOOL.write(writer, value as Boolean) + } + } + } + private fun unsupported(label: String): Nothing { throw SerializationException("Encoding of $label is not supported") } diff --git a/koltinx-serialization-scale/src/test/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/decode/binary/OptionalBinaryDecodeTest.kt b/koltinx-serialization-scale/src/test/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/decode/binary/OptionalBinaryDecodeTest.kt index 8afebd09..046a63fe 100644 --- a/koltinx-serialization-scale/src/test/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/decode/binary/OptionalBinaryDecodeTest.kt +++ b/koltinx-serialization-scale/src/test/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/decode/binary/OptionalBinaryDecodeTest.kt @@ -1,5 +1,6 @@ package io.novasama.substrate_sdk_android.koltinx_serialization_scale.decode.binary +import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.annotations.DisableOptionalBooleanOptimization import kotlinx.serialization.Serializable import org.junit.Test @@ -36,4 +37,14 @@ class OptionalBinaryDecodeTest : BinaryDecodeTest() { runDecodeTest(raw = byteArrayOf(0x01), expected = TestData(false)) runDecodeTest(raw = byteArrayOf(0x02), expected = TestData(true)) } + + @Test + fun `should decode optional boolean without optimization when annotated`() { + @Serializable + data class TestData(@DisableOptionalBooleanOptimization val a: Boolean?) + + runDecodeTest(raw = byteArrayOf(0x00), expected = TestData(null)) + runDecodeTest(raw = byteArrayOf(0x01, 0x00), expected = TestData(false)) + runDecodeTest(raw = byteArrayOf(0x01, 0x01), expected = TestData(true)) + } } \ No newline at end of file diff --git a/koltinx-serialization-scale/src/test/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/encode/binary/OptionalBinaryEncodeTest.kt b/koltinx-serialization-scale/src/test/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/encode/binary/OptionalBinaryEncodeTest.kt index 8b98a65a..b45b7324 100644 --- a/koltinx-serialization-scale/src/test/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/encode/binary/OptionalBinaryEncodeTest.kt +++ b/koltinx-serialization-scale/src/test/java/io/novasama/substrate_sdk_android/koltinx_serialization_scale/encode/binary/OptionalBinaryEncodeTest.kt @@ -1,5 +1,6 @@ package io.novasama.substrate_sdk_android.koltinx_serialization_scale.encode.binary +import io.novasama.substrate_sdk_android.koltinx_serialization_scale.binary.annotations.DisableOptionalBooleanOptimization import kotlinx.serialization.Serializable import org.junit.Test @@ -32,8 +33,18 @@ class OptionalBinaryEncodeTest : BinaryEncodeTest() { @Serializable data class TestData(val a: Boolean?) -// runEncodeTest(value = TestData(null), expected = byteArrayOf(0x00)) + runEncodeTest(value = TestData(null), expected = byteArrayOf(0x00)) runEncodeTest(value = TestData(false), expected = byteArrayOf(0x01)) -// runEncodeTest(value = TestData(true), expected = byteArrayOf(0x02)) + runEncodeTest(value = TestData(true), expected = byteArrayOf(0x02)) + } + + @Test + fun `should encode optional boolean without optimization when annotated`() { + @Serializable + data class TestData(@DisableOptionalBooleanOptimization val a: Boolean?) + + runEncodeTest(value = TestData(null), expected = byteArrayOf(0x00)) + runEncodeTest(value = TestData(false), expected = byteArrayOf(0x01, 0x00)) + runEncodeTest(value = TestData(true), expected = byteArrayOf(0x01, 0x01)) } } \ No newline at end of file