Skip to content

Commit 3f62e20

Browse files
authored
Merge pull request #26 from pactlabs/option-serialization
fix: Fix MoveOption serialization
2 parents 2dfe815 + f69b8c1 commit 3f62e20

3 files changed

Lines changed: 129 additions & 1 deletion

File tree

lib/src/commonMain/kotlin/xyz/mcxross/kaptos/model/MoveStruct.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package xyz.mcxross.kaptos.model
1818
import kotlinx.serialization.Serializable
1919
import xyz.mcxross.bcs.Bcs
2020
import xyz.mcxross.kaptos.core.Hex
21+
import xyz.mcxross.kaptos.serialize.MoveOptionSerializer
2122
import xyz.mcxross.kaptos.serialize.MoveStringSerializer
2223
import xyz.mcxross.kaptos.serialize.MoveVectorSerializer
2324

@@ -141,7 +142,7 @@ data class MoveString(val value: String) : TransactionArgument() {
141142
}
142143
}
143144

144-
@Serializable
145+
@Serializable(with = MoveOptionSerializer::class)
145146
data class MoveOption<T : EntryFunctionArgument>(val value: T?) : TransactionArgument() {
146147
fun unwrap(): T {
147148
return value ?: throw IllegalArgumentException("Option is empty")
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2024 McXross
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package xyz.mcxross.kaptos.serialize
17+
18+
import kotlinx.serialization.ExperimentalSerializationApi
19+
import kotlinx.serialization.KSerializer
20+
import kotlinx.serialization.descriptors.SerialDescriptor
21+
import kotlinx.serialization.descriptors.listSerialDescriptor
22+
import kotlinx.serialization.encoding.Decoder
23+
import kotlinx.serialization.encoding.Encoder
24+
import kotlinx.serialization.encoding.decodeStructure
25+
import kotlinx.serialization.encoding.encodeCollection
26+
import xyz.mcxross.kaptos.model.EntryFunctionArgument
27+
import xyz.mcxross.kaptos.model.MoveOption
28+
29+
class MoveOptionSerializer<T : EntryFunctionArgument>(
30+
private val elementSerializer: KSerializer<T>
31+
) : KSerializer<MoveOption<T>> {
32+
@OptIn(ExperimentalSerializationApi::class)
33+
override val descriptor: SerialDescriptor = listSerialDescriptor(elementSerializer.descriptor)
34+
35+
override fun serialize(encoder: Encoder, value: MoveOption<T>) {
36+
value.value?.let {
37+
encoder.encodeCollection(descriptor, 1) { elementSerializer.serialize(encoder, it) }
38+
} ?: run {
39+
encoder.encodeByte(0)
40+
}
41+
}
42+
43+
override fun deserialize(decoder: Decoder): MoveOption<T> {
44+
return MoveOption(
45+
decoder.decodeStructure(descriptor) {
46+
val isSome = decodeCollectionSize(descriptor)
47+
require(isSome < 2) { "Expected length of 0 or 1 for MoveOption, found $isSome" }
48+
49+
if (isSome == 1) {
50+
decodeSerializableElement(descriptor, 0, elementSerializer)
51+
} else {
52+
null
53+
}
54+
}
55+
)
56+
}
57+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package xyz.mcxross.kaptos.unit.serialize
2+
3+
import xyz.mcxross.bcs.Bcs
4+
import kotlin.test.Test
5+
import xyz.mcxross.kaptos.model.MoveOption
6+
import xyz.mcxross.kaptos.model.U8
7+
import kotlin.test.assertEquals
8+
import kotlin.test.assertFailsWith
9+
import kotlin.test.assertNull
10+
11+
class MoveOptionSerializerTest {
12+
@Test
13+
fun `can deserialize a Some option (vector of length 1)`() {
14+
val encoded = listOf(1.toByte(), 42.toByte()).toByteArray()
15+
16+
val decoded: MoveOption<U8> = Bcs.decodeFromByteArray(encoded)
17+
val expected = MoveOption(U8(42.toByte()))
18+
19+
assertEquals(expected.value?.value, decoded.value?.value)
20+
}
21+
22+
@Test
23+
fun `can deserialize a None option (vector of length 0)`() {
24+
val encoded = listOf(0.toByte()).toByteArray()
25+
26+
val decoded: MoveOption<U8> = Bcs.decodeFromByteArray(encoded)
27+
val expected = MoveOption<U8>(null)
28+
29+
assertNull(decoded.value)
30+
assertEquals(expected.value, decoded.value)
31+
}
32+
33+
@Test
34+
fun `unwrap returns value when Some`() {
35+
val encoded = listOf(1.toByte(), 100.toByte()).toByteArray()
36+
37+
val decoded: MoveOption<U8> = Bcs.decodeFromByteArray(encoded)
38+
39+
assertEquals(100.toByte(), decoded.unwrap().value)
40+
}
41+
42+
@Test
43+
fun `unwrap throws when None`() {
44+
val encoded = listOf(0.toByte()).toByteArray()
45+
46+
val decoded: MoveOption<U8> = Bcs.decodeFromByteArray(encoded)
47+
48+
assertFailsWith<IllegalArgumentException> {
49+
decoded.unwrap()
50+
}
51+
}
52+
53+
@Test
54+
fun `should throw error when deserializing empty byte array`() {
55+
val emptyInput = byteArrayOf()
56+
57+
assertFailsWith<Exception> {
58+
Bcs.decodeFromByteArray<MoveOption<U8>>(emptyInput)
59+
}
60+
}
61+
62+
@Test
63+
fun `should throw error when deserializing vector with length greater than 1`() {
64+
val encoded = listOf(2.toByte(), 1.toByte(), 2.toByte()).toByteArray()
65+
66+
assertFailsWith<IllegalArgumentException> {
67+
Bcs.decodeFromByteArray<MoveOption<U8>>(encoded)
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)