機能

シリアライザー

ブロックチェーンにデータを送信する際も読み込む際も、シリアライゼーションはプロセスの重要な部分です。シリアライゼーションのロジックはプログラムによって異なることがあり、Borshシリアライゼーションがsolanaプログラムで最も人気のある選択肢ですが、唯一の選択肢ではありません。

Umiは、独自のシリアライザーを構築できる柔軟で拡張可能なシリアライゼーションフレームワークを提供することでこれを支援します。具体的には、以下が含まれます:

  • FromUint8Arrayにシリアライズし、Uint8ArrayTo(デフォルトはFrom)にデシリアライズできるオブジェクトを表すジェネリックSerializer<From, To = From>タイプ。
  • シリアライザーを新しいシリアライザーにマップおよび変換するシリアライザーヘルパーの束。
  • 最後に、文字列エンコーダー、数値シリアライザー、データ構造などの一般的なタイプをシリアライズするために使用できる組み込みシリアライザーのセット。これらのプリミティブを使用して、より複雑なシリアライザーを構築できます。

すべての仕組みを見てみましょう。

シリアライザー定義

Serializerタイプは、Umiのシリアライゼーションフレームワークの中心的な部分です。タイプTSerializerインスタンスがあれば、Tのインスタンスをシリアライズおよびデシリアライズするために必要なすべてが揃っているはずです。たとえば、Serializer<{ name: string, age: number }>インスタンスを使用して、{ name: string, age: number }のインスタンスをシリアライズおよびデシリアライズできます。

場合によっては、シリアライズしたいデータが、デシリアライズ時に取得するデータよりもわずかに緩い場合があります。この理由により、Serializer<From, To>タイプは、Fromを拡張し、デフォルトでFromとなる2番目のタイプパラメーターToを許可します。前の例を使用すると、age属性がオプションで、提供されない場合は42にデフォルト設定されることを想像してください。その場合、{ name: string, age?: number }Uint8Arrayにシリアライズするが、Uint8Array{ name: string, age: number }にデシリアライズするSerializer<{ name: string, age?: number }, { name: string, age: number }>インスタンスを定義できます。

以下がSerializerタイプの定義方法です。

type Serializer<From, To extends From = From> = {
/** シリアライザーの説明。 */
description: string;
/** シリアライズされた値の固定サイズ(バイト単位)、または可変の場合は`null`。 */
fixedSize: number | null;
/** シリアライズされた値が取ることができる最大サイズ(バイト単位)、または可変の場合は`null`。 */
maxSize: number | null;
/** 値をバイトにシリアライズする関数。 */
serialize: (value: From) => Uint8Array;
/**
* バイトから値をデシリアライズする関数。
* デシリアライズされた値と読み取られたバイト数を返します。
*/
deserialize: (buffer: Uint8Array, offset?: number) => [To, number];
};

驚くべきことではないserializeおよびdeserialize関数に加えて、SerializerタイプにはdescriptionfixedSizemaxSizeも含まれます。

  • descriptionは、シリアライザーを説明する簡単な人間が読める文字列です。
  • fixedSize属性は、固定サイズシリアライザーを扱っている場合にのみ、シリアライズされた値のサイズをバイト単位で示します。たとえば、u32シリアライザーは常に4バイトのfixedSizeを持ちます。
  • maxSize属性は、取ることができる最大サイズに境界がある可変サイズシリアライザーを扱っている場合に役立ちます。たとえば、borsh Option<PublicKey>シリアライザーは、サイズ1または33のいずれかになることができ、したがって33バイトのmaxSizeを持ちます。

シリアライザーの使用

Umiフレームワークにバンドルされている@metaplex-foundation/umi/serializersサブモジュールからSerializerタイプとシリアライザー関連のすべてをインポートできます。また、フレームワークの残りの部分なしで使用したい場合は、スタンドアロンの@metaplex-foundation/umi-serializersライブラリとしてインポートすることもできます。

// Umiにバンドル。
import { Serializer } from '@metaplex-foundation/umi/serializers';
// スタンドアロンライブラリとして。
import { Serializer } from '@metaplex-foundation/umi-serializers';

インポートしたら、Umiが提供するすべての組み込みシリアライザーとヘルパーを使用できます。以下のセクションでそれぞれを詳しく見ていきますが、今のところ、それらがどのように機能するかを見るための簡単な例を見てみましょう。name属性(stringタイプ)、publicKey属性(PublicKeyタイプ)、numbers属性(number[]タイプ、各数値はu32整数)を含むさまざまな属性を持つMyObjectタイプがあったとしましょう。以下は、そのためのシリアライザーを作成する方法です。

import { PublicKey } from '@metaplex-foundation/umi';
import { Serializer, struct, string, publicKey, array, u32 } from '@metaplex-foundation/umi/serializers';
type MyObject = {
name: string;
publicKey: PublicKey;
numbers: number[];
};
const mySerializer: Serializer<MyObject> = struct([
['name', string()],
['publicKey', publicKey()],
['numbers', array(u32())],
]);

提供された各シリアライザーは独自の引数を定義しますが(たとえば、array関数は最初の引数としてアイテムシリアライザーを必要とします)、そのほとんどは最後にオプションのoptions引数を持ち、シリアライザーの動作を調整するために使用できます。options引数内の属性はシリアライザーによって異なる場合がありますが、それらはすべて1つの共通属性を共有します:description。これを使用して、作成されたシリアライザーの特定の説明を提供できます。省略された場合、十分な説明が作成されることに注意してください。

import { string } from '@metaplex-foundation/umi/serializers';
string().description; // -> 'string(utf8; u32(le))'.
string({ description: 'My custom string description' });

シリアライザーヘルパー

シリアライザーをインポートして使用する方法がわかったところで、Umiがそれらを変換するために提供するヘルパーメソッドのいくつかを見てみましょう。

シリアライザーのマッピング

mapSerializerを使用すると、BAに変換し、ABに戻す2つの関数を提供することで、Serializer<A>Serializer<B>に変換できます。

たとえば、文字列の長さを格納することで、文字列シリアライザーを数値シリアライザーに変換したいとします。以下は、mapSerializer関数を使用してそれを行う方法です。

const serializerA: Serializer<string> = ...;
const serializerB: Serializer<number> = mapSerializer(
serializerA,
(value: number): string => 'x'.repeat(value), // 指定された長さのモック文字列を作成。
(value: string): number => value.length, // 文字列の長さを取得。
);

mapSerializerは、異なるFromおよびToタイプを持つシリアライザーを変換するためにも使用できます。以下は上記の例と似ていますが、異なるToタイプです。

const serializerA: Serializer<string | null, string> = ...;
const serializerB: Serializer<number | null, number> = mapSerializer(
serializerA,
(value: number | null): string | null => value === null ? null : 'x'.repeat(value),
(value: string): number => value.length,
);

Toタイプを変更せずに、シリアライザーのFromタイプのみを変換することに興味がある場合は、代わりに1つの関数のみでmapSerializer関数を使用できることに注意してください。以下は、Serializer<{ name: string, age: number }>インスタンスを緩めて、シリアライズ時のみage属性をオプションにする方法です。

type Person = { name: string, age: number };
type PersonWithOptionalAge = { name: string, age?: number };
const serializerA: Serializer<Person> = ...;
const serializerB: Serializer<PersonWithOptionalAge, Person> = mapSerializer(
serializerA,
(value: PersonWithOptionalAge): Person => ({
name: value.name,
age: value.age ?? 42,
}),
);

シリアライザーのマッピングは、組み込みシリアライザーに依存しながら複雑なユースケースを構築するのに役立つ非常に強力な技術です。

シリアライザーの固定

fixSerializer関数は、固定サイズをバイト単位で要求することで、可変サイズシリアライザーを固定サイズシリアライザーに変換できる別のヘルパーです。必要に応じてUint8Arrayバッファーを要求されたサイズにパディングまたは切り詰めることでこれを行います。返されるシリアライザーは、元のシリアライザーと同じFromおよびToタイプを持ちます。

const myFixedSerializer = fixSerializer(myVariableSerializer, 42);

シリアライザーの逆転

reverseSerializer関数を使用すると、固定サイズシリアライザーのバイトを逆転させることができます。この関数のアプリケーションはあまり頻繁ではありませんが、たとえばエンディアンネスを扱う際に役立つことがあります。ここでも、返されるシリアライザーは元のシリアライザーと同じFromおよびToタイプを持ちます。

const myReversedSerializer = reverseSerializer(mySerializer);

バイトヘルパー

バイトを操作するための低レベルヘルパーメソッドも提供されることは注目に値します。これらはシリアライザーを返しませんが、カスタムシリアライザーを構築する際に役立つことがあります。

// 複数のUint8Arrayバッファーを1つにマージ。
mergeBytes([new Uint8Array([1, 2]), new Uint8Array([3, 4])]); // -> Uint8Array([1, 2, 3, 4])
// Uint8Arrayバッファーを指定されたサイズにパディング。
padBytes(new Uint8Array([1, 2]), 4); // -> Uint8Array([1, 2, 0, 0])
padBytes(new Uint8Array([1, 2, 3, 4]), 2); // -> Uint8Array([1, 2, 3, 4])
// Uint8Arrayバッファーを指定されたサイズにパディングおよび切り詰め。
fixBytes(new Uint8Array([1, 2]), 4); // -> Uint8Array([1, 2, 0, 0])
fixBytes(new Uint8Array([1, 2, 3, 4]), 2); // -> Uint8Array([1, 2])

組み込みシリアライザー

それでは、Umiに付属するさまざまなシリアライザーを見てみましょう。これらの各プリミティブは、前のセクションで見たように、より複雑なシリアライザーを構築するために使用できます。

数値

Umiには12の数値シリアライザーが付属しています:5つの符号なし整数、5つの符号付き整数、2つの浮動小数点数。これらは異なるサイズの数値をシリアライズおよびデシリアライズするために使用できます。数値のサイズが32ビットより大きい場合、JavaScriptのネイティブnumberタイプは2^53 - 1より大きな数値をサポートしないため、返されるシリアライザーはSerializer<number>ではなくSerializer<number | bigint, bigint>になります。

// 符号なし整数。
u8(); // -> Serializer<number>
u16(); // -> Serializer<number>
u32(); // -> Serializer<number>
u64(); // -> Serializer<number | bigint, bigint>
u128(); // -> Serializer<number | bigint, bigint>
// 符号付き整数。
i8(); // -> Serializer<number>
i16(); // -> Serializer<number>
i32(); // -> Serializer<number>
i64(); // -> Serializer<number | bigint, bigint>
i128(); // -> Serializer<number | bigint, bigint>
// 浮動小数点数。
f32(); // -> Serializer<number>
f64(); // -> Serializer<number>

1バイトのみを使用するu8およびi8シリアライザーを除き、他のすべての数値シリアライザーはデフォルトでリトルエンディアンで表現され、異なるエンディアンネスを使用するように設定できます。これは、シリアライザーにendianオプションを渡すことで行えます。

u64(); // リトルエンディアン。
u64({ endian: Endian.Little }); // リトルエンディアン。
u64({ endian: Endian.Big }); // ビッグエンディアン。

数値シリアライザーは他のシリアライザーでしばしば再利用されるため、Umiはnumberおよびbigintタイプの両方を含む以下のNumberSerializerタイプを定義することに注意してください。

type NumberSerializer =
| Serializer<number>
| Serializer<number | bigint, bigint>;

ブール値

boolシリアライザーを使用してSerializer<boolean>を作成できます。デフォルトでは、ブール値を格納するためにu8数値を使用しますが、これはsizeオプションにNumberSerializerを渡すことで変更できます。

bool(); // -> u8を使用。
bool({ size: u32() }); // -> u32を使用。
bool({ size: u32({ endian: Endian.Big }) }); // -> ビッグエンディアンu32を使用。

文字列エンコーディング

Umiには、異なる形式で文字列をシリアライズおよびデシリアライズするために使用できる以下の文字列シリアライザーが付属しています:utf8base10base16base58base64

utf8.serialize('Hello World!');
base10.serialize('42');
base16.serialize('ff002a');
base58.serialize('LorisCg1FTs89a32VSrFskYDgiRbNQzct1WxyZb7nuA');
base64.serialize('SGVsbG8gV29ybGQhCg==');

また、指定されたアルファベットに対して新しい文字列シリアライザーを作成できるbaseX関数も付属しています。たとえば、以下はbase58シリアライザーの実装方法です。

const base58: Serializer<string> = baseX(
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
);

文字列

stringシリアライザーは、さまざまなエンコーディングとサイズ戦略を使用して文字列をシリアライズするために使用できるSerializer<string>を返します。以下のオプションが含まれます:

  • encoding: 文字列をシリアライズおよびデシリアライズする際に使用するエンコーディングを表すSerializer<string>。デフォルトは組み込みのutf8シリアライザーです。なぜSerializer<string>を渡してSerializer<string>を作成する必要があるのかと疑問に思うかもしれませんが、これはencodingシリアライザーの目的が、文字列のサイズを格納するなどの他のことを心配せずに、テキストをバイト配列との間で変換することだけだからです。これにより、他のすべてのオプションを活用しながら、任意のエンコーディングを接続できます。
  • size: 指定されたバッファーで文字列がどこまで続くかを知るためには、そのサイズをバイト単位で知る必要があります。そのために、以下のサイズ戦略のいずれかを使用できます:
    • NumberSerializer: 数値シリアライザーが渡されると、文字列のサイズを格納および復元するためのプレフィックスとして使用されます。デフォルトでは、サイズはリトルエンディアンのu32プレフィックスを使用して格納されます(これはborshシリアライゼーションのデフォルト動作です)。
    • number: バイトサイズは明示的に数値として提供することもできます。これにより、サイズプレフィックスを使用せず、常に同じバイト数を使用して文字列を格納する固定サイズシリアライザーが作成されます。
    • "variable": サイズとして文字列"variable"が渡されると、デシリアライズ時にバッファー内の残りのすべてのバイトを使用する可変サイズシリアライザーが作成されます。シリアライズ時には、シリアライズされた文字列のサイズを格納せずに、単にencodingシリアライザーの結果を返します。
// 参考用の異なるエンコーディングを使用したシリアライズされた値。
utf8.serialize('Hi'); // -> 0x4869
base58.serialize('Hi'); // -> 0x03c9
// デフォルト動作:utf8エンコーディングとu32(リトルエンディアン)サイズ。
string().serialize('Hi'); // -> 0x020000004869
// カスタムエンコーディング:base58。
string({ encoding: base58 }).serialize('Hi'); // -> 0x0200000003c9
// カスタムサイズ:u16(ビッグエンディアン)サイズ。
string({ size: u16({ endian: Endian.Big }) }).serialize('Hi'); // -> 0x00024869
// カスタムサイズ:5バイト。
string({ size: 5 }).serialize('Hi'); // -> 0x4869000000
// カスタムサイズ:可変。
string({ size: 'variable' }).serialize('Hi'); // -> 0x4869

バイト

bytesシリアライザーは、Uint8Arrayを...Uint8ArrayにデシリアライズするSerializer<Uint8Array>を返します。これは少し無用に思えるかもしれませんが、他のシリアライザーに構成される際に役立つことがあります。たとえば、特定のフィールドがシリアライズされずに残されるべきことを示すために、structシリアライザーで使用できます。

string関数と非常に似て、bytes関数には、バイト配列のサイズがどのように格納および復元されるかを設定するsizeオプションが含まれます。ここでのデフォルトサイズが"variable"戦略であることを除いて、string関数と同じサイズ戦略がサポートされています。要約すると:

  • NumberSerializer: バイト配列のサイズを格納および復元するためにプレフィックス数値シリアライザーを使用します。
  • number: バイト配列を格納するために固定サイズを使用します。
  • "variable": シリアライズ時にバッファーをそのまま渡し、デシリアライズ時にバッファーの残りを返します。デフォルト動作。
// デフォルト動作:可変サイズ。
bytes().serialize(new Uint8Array([42])); // -> 0x2a
// カスタムサイズ:u16(リトルエンディアン)サイズ。
bytes({ size: u16() }).serialize(new Uint8Array([42])); // -> 0x01002a
// カスタムサイズ:5バイト。
bytes({ size: 5 }).serialize(new Uint8Array([42])); // -> 0x2a00000000

公開キー

publicKeyシリアライザーは、公開キーをシリアライズおよびデシリアライズするために使用できるSerializer<PublicKey>を返します。以下は、同じ公開キーをシリアライズおよびデシリアライズする例です。publicKey関数はメイン@metaplex-foundation/umiパッケージからもエクスポートされ、さまざまな入力から公開キーを作成できることに注意してください。したがって、競合を避けるためにインポートをエイリアスする必要がある場合があります。

import { publicKey } from '@metaplex-foundation/umi';
import { publicKey as publicKeySerializer } from '@metaplex-foundation/umi/serializers';
const myPublicKey = publicKey('...');
const buffer = publicKeySerializer().serialize(myPublicKey);
const [myDeserializedPublicKey, offset] = publicKeySerializer().deserialize(buffer);
myPublicKey === myDeserializedPublicKey; // -> true

単位

unitシリアライザーは、undefinedを空のUint8Arrayにシリアライズし、デシリアライズ時にバイトを消費せずにundefinedを返すSerializer<void>を返します。これは他のシリアライザーによって内部的に使用できるより低レベルのシリアライザーです。たとえば、これがdataEnumシリアライザーが内部的に空のバリアントを記述する方法です。

unit().serialize(undefined); // -> new Uint8Array([])
unit().deserialize(new Uint8Array([42])); // -> [undefined, 0]

配列、セット、マップ

Umiは、リストとマップをシリアライズするための3つの関数を提供します:

  • array: アイテムの配列をシリアライズします。引数としてSerializer<T>を受け取り、Serializer<T[]>を返します。
  • set: 一意のアイテムのセットをシリアライズします。引数としてSerializer<T>を受け取り、Serializer<Set<T>>を返します。
  • map: キー値ペアのマップをシリアライズします。キー用のSerializer<K>と値用のSerializer<V>を引数として受け取り、Serializer<Map<K, V>>を返します。

3つの関数はすべて、配列、セット、マップの長さがどのように格納および復元されるかを設定する同じsizeオプションを受け取ります。これはstringおよびbytesシリアライザーの動作と非常に似ています。サポートされている戦略は以下の通りです:

  • NumberSerializer: コンテンツの前にそのサイズを付けた数値シリアライザーを使用します。デフォルトでは、サイズはリトルエンディアンのu32プレフィックスを使用して格納されます。
  • number: 固定数のアイテムを持つ配列、セット、マップシリアライザーを返します。
  • "remainder": バッファーの残りをアイテムの固定サイズで割ることによってアイテム数を推定する配列、セット、マップシリアライザーを返します。たとえば、バッファーに64バイトが残っており、配列の各アイテムが16バイトの長さの場合、配列は4つのアイテムでデシリアライズされます。このオプションは固定サイズアイテムにのみ使用可能であることに注意してください。マップの場合、キーシリアライザーと値シリアライザーの両方が固定サイズである必要があります。
// 配列。
array(u8()) // u32サイズプレフィックス付きのu8アイテムの配列。
array(u8(), { size: 5 }) // 5つのu8アイテムの配列。
array(u8(), { size: 'remainder' }) // 可変サイズのu8アイテムの配列。
// セット。
set(u8()) // u32サイズプレフィックス付きのu8アイテムのセット。
set(u8(), { size: 5 }) // 5つのu8アイテムのセット。
set(u8(), { size: 'remainder' }) // 可変サイズのu8アイテムのセット。
// マップ。
map(u8(), u8()) // u32サイズプレフィックス付きの(u8, u8)エントリのマップ。
map(u8(), u8(), { size: 5 }) // 5つの(u8, u8)エントリのマップ。
map(u8(), u8(), { size: 'remainder' }) // 可変サイズの(u8, u8)エントリのマップ。

オプションとヌル許容値

Umiは、オプション値をシリアライズするための2つの関数を提供します:

  • nullable: nullになる可能性のある値をシリアライズします。引数としてSerializer<T>を受け取り、Serializer<Nullable<T>>を返します。ここでNullable<T>T | nullのタイプエイリアスです。
  • option: Optionインスタンスをシリアライズします(ドキュメントを参照)。引数としてSerializer<T>を受け取り、Serializer<OptionOrNullable<T>, Option<T>>を返します。これは、デシリアライズされた値は常にOptionタイプでラップされますが、シリアライズされた値はOption<T>またはNullable<T>のいずれかになることを意味します。

両方の関数は、値が存在するかどうかを示すブール値を前に付けることでオプション値をシリアライズします。プレフィックスブール値がfalseの場合、値はnull(ヌル許容値の場合)またはNone(オプションの場合)であり、実際の値のデシリアライズをスキップできます。そうでない場合、値は提供されたシリアライザーを使用してデシリアライズされ、返されます。

両方とも、作成されたシリアライザーの動作を設定するための同じオプションを提供します:

  • prefix: ブールプレフィックスをシリアライズおよびデシリアライズするために使用するNumberSerializer。デフォルトでは、リトルエンディアンのu8プレフィックスを使用します。
  • fixed: これがtrueの場合、値が空の場合のシリアライゼーションロジックを変更することで固定サイズシリアライザーを返します。この場合、シリアライズされた値はゼロでパディングされ、空の値と満たされた値が同じバイト数を使用してシリアライズされます。これはアイテムシリアライザーが固定サイズの場合にのみ機能することに注意してください。
// オプション。
option(publicKey()) // u8プレフィックス付きのOption<PublicKey>。
option(publicKey(), { prefix: u16() }) // u16プレフィックス付きのOption<PublicKey>。
option(publicKey(), { fixed: true }) // 固定サイズのOption<PublicKey>。
// ヌル許容値。
nullable(publicKey()) // u8プレフィックス付きのNullable<PublicKey>。
nullable(publicKey(), { prefix: u16() }) // u16プレフィックス付きのNullable<PublicKey>。
nullable(publicKey(), { fixed: true }) // 固定サイズのNullable<PublicKey>。

構造体

structシリアライザーは、ジェネリック型TのJavaScriptオブジェクトをシリアライズおよびデシリアライズできます。

最初の引数で各フィールドの名前とシリアライザーを配列として渡す必要があります。このfields配列は、各フィールドがタプルであり、最初の項目がフィールドの名前、2番目の項目がフィールドのシリアライザーとなるように構成されています。フィールドの順序は重要です。なぜなら、フィールドがシリアライズおよびデシリアライズされる順序を決定するからです。以下に例を示します。

type Person = {
name: string;
age: number;
}
struct<Person>([
['name', string()],
['age', u32()],
]);

struct関数は、一部のフィールドが異なるFromおよびToタイプパラメーターを持つ場合に備えて、2番目のタイプパラメーターUも受け取ります。これにより、Serializer<T, U>タイプのシリアライザーを作成できます。

たとえば、以下はPersonタイプのageフィールドにデフォルト値を提供する構造体シリアライザーを作成する方法です。

type Person = { name: string; age: number; }
type PersonArgs = { name: string; age?: number; }
const ageOr42 = mapSerializer(
u32(),
(age: number | undefined): number => age ?? 42,
);
struct<PersonArgs, Person>([
['name', string()],
['age', ageOr42],
]);

タプル

Umiは、タプルをシリアライズおよびデシリアライズするために使用できるtupleシリアライザーを提供します。タプルはJavaScriptではネイティブではありませんが、各アイテムが独自の定義されたタイプを持つ配列を使用してTypeScriptで表現できます。たとえば、Rustの(String, u8)タプルはTypeScriptでは[string, number]として表現できます。

tuple関数は、最初の引数として、タプルのアイテムと同じ順序で一致するシリアライザーの配列を受け取ります。以下にいくつかの例を示します。

tuple([bool()]); // Serializer<[bool]>
tuple([string(), u8()]); // Serializer<[string, number]>
tuple([publicKey(), u64()]); // Serializer<[PublicKey, number | bigint], [PublicKey, bigint]>

スカラー列挙

scalarEnum関数は、バリアントの値(またはインデックス)をu8数値として格納することで、スカラー列挙のシリアライザーを作成するために使用できます。

最初の引数として列挙コンストラクターが必要です。たとえば、列挙がenum Direction { Left }として定義されている場合、コンストラクターDirectionを最初の引数として渡す必要があります。作成されたシリアライザーは、列挙のバリアント、その値、またはその名前を入力として受け取ります。以下に例を示します。

enum Direction { Left, Right, Up, Down };
const directionSerializer = scalarEnum(Direction); // Serializer<Direction>
directionSerializer.serialize(Direction.Left); // -> 0x00
directionSerializer.serialize(Direction.Right); // -> 0x01
directionSerializer.serialize('Left'); // -> 0x00
directionSerializer.serialize('Right'); // -> 0x01
directionSerializer.serialize(0); // -> 0x00
directionSerializer.serialize(1); // -> 0x01
// デシリアライズされた値は常に列挙のインスタンスです。
directionSerializer.deserialize(new Uint8Array([1])); // -> [Direction.Right, 1]

シリアライズされた値はデフォルトでu8数値シリアライザーを使用して格納されますが、sizeオプションとしてカスタムNumberSerializerを提供してその動作を変更できます。

scalarEnum(Direction, { size: u32() }).serialize(Direction.Right); // -> 0x01000000

文字列列挙(例:enum Direction { Left = 'LEFT' })でscalarEnum関数を使用する場合、テキスト値は無視され、バリアントのインデックスのみが使用されることに注意してください。

enum Direction { Left = 'LEFT', Right = 'RIGHT', Up = 'UP', Down = 'DOWN' };
const directionSerializer = scalarEnum(Direction); // Serializer<Direction>
directionSerializer.serialize(Direction.Left); // -> 0x00
directionSerializer.serialize('Left'); // -> 0x00
// 列挙文字列値を入力として使用できることに注意してください。
directionSerializer.serialize('LEFT'); // -> 0x00

データ列挙

Rustでは、列挙は強力なデータ型であり、そのバリアントは以下のいずれかになることができます:

  • 空のバリアント — 例:enum Message { Quit }
  • タプルバリアント — 例:enum Message { Write(String) }
  • 構造体バリアント — 例:enum Message { Move { x: i32, y: i32 } }

JavaScriptにはそのような強力な列挙はありませんが、各オブジェクトが特定のフィールドによって区別されるオブジェクトの共用体を使用して、TypeScriptでそれらをエミュレートできます。これをデータ列挙と呼びます。

Umiでは、__kindフィールドを使用してデータ列挙の異なるバリアントを区別します。さらに、すべてのバリアントがオブジェクトであるため、fieldsプロパティを使用してタプルバリアントの配列をラップします。以下に例を示します。

type Message =
| { __kind: 'Quit' } // 空のバリアント。
| { __kind: 'Write'; fields: [string] } // タプルバリアント。
| { __kind: 'Move'; x: number; y: number }; // 構造体バリアント。

dataEnum関数は、データ列挙のシリアライザーを作成できます。最初の引数として各バリアントの名前とシリアライザーが必要です。structシリアライザーと同様に、これらはバリアントタプルの配列として定義され、最初の項目はバリアントの名前、2番目の項目はバリアントのシリアライザーです。空のバリアントはシリアライズするデータがないため、単にunitシリアライザーを使用します。以下は前の例のデータ列挙シリアライザーを作成する方法です。

const messageSerializer = dataEnum<Message>([
// 空のバリアント。
['Quit', unit()],
// タプルバリアント。
['Write', struct<{ fields: [string] }>([
['fields', tuple([string()])]
])],
// 構造体バリアント。
['Move', struct<{ x: number; y: number }>([
['x', i32()],
['y', i32()]
])],
]);

このシリアライゼーションはRust列挙のborshシリアライゼーションと互換性があることに注意してください。まず、バリアントのインデックスを格納するためにリトルエンディアンでu32数値を使用します。選択されたバリアントが空のバリアントの場合、そこで停止します。そうでない場合、バリアントのシリアライザーを使用してそのデータをシリアライズします。

messageSerializer.serialize({ __kind: 'Quit' }); // -> 0x00000000
messageSerializer.serialize({ __kind: 'Write', fields: ['Hi'] }); // -> 0x01000000020000004869
messageSerializer.serialize({ __kind: 'Move', x: 5, y: 6 }); // -> 0x020000000500000006000000

dataEnum関数は、上記のデフォルトのu32の代わりに、バリアントインデックス用のカスタム数値シリアライザーを選択できるprefixオプションも受け取ります。以下はu32の代わりにu8を使用する例です。

const messageSerializer = dataEnum<Message>([...], {
prefix: u8()
});
messageSerializer.serialize({ __kind: 'Quit' }); // -> 0x00
messageSerializer.serialize({ __kind: 'Write', fields: ['Hi'] }); // -> 0x01020000004869
messageSerializer.serialize({ __kind: 'Move', x: 5, y: 6 }); // -> 0x020500000006000000

データ列挙を扱う際は、Rustの列挙処理方法により近い感じになるように開発者エクスペリエンスを向上させるヘルパーメソッドを提供したい場合があることに注意してください。これはKinobiが生成されたJavaScriptクライアントにすぐに提供するものです。

// ヘルパーメソッドの例。
message('Quit'); // -> { __kind: 'Quit' }
message('Write', ['Hi']); // -> { __kind: 'Write', fields: ['Hi'] }
message('Move', { x: 5, y: 6 }); // -> { __kind: 'Move', x: 5, y: 6 }
isMessage('Quit', message('Quit')); // -> true
isMessage('Write', message('Quit')); // -> false

ビット配列

bitArrayシリアライザーは、各ブール値が単一ビットで表現されるようにブール値の配列をシリアライズおよびデシリアライズするために使用できます。シリアライザーのサイズ(バイト単位)と、ビットの順序を逆にするために使用できるオプションのbackwardフラグが必要です。

const booleans = [true, false, true, false, true, false, true, false];
bitArray(1).serialize(booleans); // -> Uint8Array.from([0b10101010]);
bitArray(1).deserialize(Uint8Array.from([0b10101010])); // -> [booleans, 1];
Previous
RPC