Using codec

To get a default codec do this:

val codec: ScaleCodec = ScaleCodec.default()

You might instantiate it using custom settings, but all examples will cover common interface of a codec.

It's very easy as that to encode or decode objects like this:

val codec: ScaleCodec
val int: Int32

val encoded: ByteArray = codec.toScale(int, Int32::class)
val decoded: Int32 = codec.fromScale(encoded, Int32::class)
// decoded == int

Same interface applies to every possible type.

But once you touch enums or structs from Rust declarations, things become more complicated.

Enums

Kotlin enums are tied to Java's enums, thus are not so powerful as Rust ones.

If you mirror basic enums, that's still easy. You can declare your enum like this, and operate with a codec:

enum BasicEnumType {
    FirstCase,
    SecondCase
}

val codec: ScaleCodec
val myBasicEnum: BasicEnumType = BasicEnumType.FirstCase

val encoded: ByteArray = codec.toScale(myBasicEnum, BasicEnumType::class)
// encoded = [0], basically "index" of a case

val decoded: BasicEnumType = codec.fromScale(encoded, BasicEnumType::class)
// decoded == myBasicEnum

But when you want to implement non-plain enums you can't use Kotlin enums. Therefore, we found a workaround that would help you to mirror enums from Rust:

@EnumClass // optional annotation
sealed class PowerfulEnumType {
    @EnumCase(0) data class FirstEnumKind(value: Int32): PowerfulEnumType()
    @EnumCase(1) class SecondEnumKind: PowerfulEnumType()
} 

val codec: ScaleCodec
val myPowerfulEnum: PowerfulEnumType = PowerfulEnumType.FirstEnumKind(0)

val encoded: ByteArray = codec.toScale(myPowerfulEnum, PowerfulEnumType::class)
// encodes enum case "0", and then actual value inside the data class
// basically, it treats internal type as separate type which is resolved as Rust's "struct"

val decoded: PowerfulEnumType = codec.fromScale(encoded, PowerfulEnumType::class)
// decoded == myPowerfulEnum

Please look at annotations EnumClass and EnumCase.

First, EnumClass is optional, our codec may detect your enum without it. But if you provide this annotation, it will resolve it much faster, so you might want to put it on every of your enum.

Second, EnumCase is required always. We're using Kotlin reflection. And unfortunately, when getting children types of your sealed class, all children types might appear in random order. Thus, you need to strictly provided this "case" index, so we encode and decode this in a proper way.

And finally, every your enum case should inherit from "super" enum sealed class type. This is required for encoding purpose. Otherwise, we couldn't find its supertype to properly decode this. Because if super type is unknown, we can't back this to this supertype on return.

Structs, classes

Classes are more complicated in Kotlin reflection. Thus we have strong limitations for declaring Rust struct mirrors in Kotlin.

You can either use data classes or plain classes.

For data classes, only variables from constructor will be read. So all of them should be declared as properties, not just injections. While you can make them private and provide separate variable inside data class body which won't be handled in serialization.

data class MyCustomType(
    val string: String,
    internal indexUInt8: UInt8
) {
    // this variable won't participate in serialization
    // and since [UInt8]/[UByte] is not very useful in Kotlin,
    // you can convert it to more useful type [UInt]
    val index: UInt get() = indexUInt8.toUInt()
}

For regular classes, you can use whatever you want in constructor, as this will be ignored in our serialization. This is reserved by our codec for a purpose, when you want custom primary constructor, which takes different data, and then accumulates this to different class variables.

So all the variables will participate in serialization. We still think that some @ScaleIgnore annotation for some variables might be useful in this case, but not yet implemented.

class MyCustomType(
    // this will be ignored in serialization
    index: UInt
) {
    // this type will be used for both read and write, 
    // even if it's `val`, Kotlin reflection is OK with that
    internal val indexUInt8: UInt8 = index.toUByte()
}

In most cases we find data classes more useful, so we encourage you to use this. As we don't see a reason why complex classes should be used within the codec, and only small (or not so small, but at least "simple") data structures should be codec-ready entities.

Fixed size arrays

Even though the Kotlin is a very powerful and wonderful language, it misses fixed size arrays.

This is one of the features in Substrate, and is used extensively.

To declare fixed size array as one of the variables, please use next syntax:

data class MyTypeWithFixedArray(
    val string: String,
    @FixedArray(size = 32) val accountId: AccountId 
)

Last updated