Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
refactor(experimental): support custom discriminator property for get…
Browse files Browse the repository at this point in the history
…DataEnumCodec (#2380)

This PR adds support for custom discriminator properties for `getDataEnumCodecs`.

```ts
// Before.
const codec = getDataEnumCodec([
  ['click', getStructCodec([[['x', u32], ['y', u32]]])],
  ['keyPress', getStructCodec([[['key', u32]]])]
]);

codec.encode({ __kind: 'click', x: 1, y: 2 });
codec.encode({ __kind: 'keyPress', key: 3 });

// After.
const codec = getDataEnumCodec([
  ['click', getStructCodec([[['x', u32], ['y', u32]]])],
  ['keyPress', getStructCodec([[['key', u32]]])]
], { discriminator: 'event' });

codec.encode({ event: 'click', x: 1, y: 2 });
codec.encode({ event: 'keyPress', key: 3 });
```

Note that, to make this work, this PR updates a few exported types such as `GetDataEnumKind` or `GetDataEnumKindContent` which is used by Kinobi. I'll make sure to have a PR ready on Kinobi's side when this is ready to be merged.
  • Loading branch information
lorisleiva authored Mar 27, 2024
1 parent 3c33220 commit bf029dd
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 91 deletions.
15 changes: 15 additions & 0 deletions .changeset/violet-brooms-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@solana/codecs-data-structures': patch
---

DataEnum codecs now support custom discriminator properties

```ts
const codec = getDataEnumCodec([
['click', getStructCodec([[['x', u32], ['y', u32]]])],
['keyPress', getStructCodec([[['key', u32]]])]
], { discriminator: 'event' });

codec.encode({ event: 'click', x: 1, y: 2 });
codec.encode({ event: 'keyPress', key: 3 });
```
22 changes: 17 additions & 5 deletions packages/codecs-data-structures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ In Rust, enums are powerful data types whose variants can be one of the followin

Whilst we do not have such powerful enums in JavaScript, we can emulate them in TypeScript using a union of objects such that each object is differentiated by a specific field. **We call this a data enum**.

We use a special field named `__kind` to distinguish between the different variants of a data enum. Additionally, since all variants are objects, we use a `fields` property to wrap the array of tuple variants. Here is an example.
We use a special field named `__kind` to distinguish between the different variants of a data enum. Additionally, since all variants are objects, we can use a `fields` property to wrap the array of tuple variants. Here is an example.

```ts
type Message =
Expand All @@ -264,7 +264,7 @@ type Message =

The `getDataEnumCodec` function helps us encode and decode these data enums.

It requires the name and codec of each variant as a first argument. Similarly to the struct codec, these are defined as an array of variant tuples where the first item is the name of the variant and the second item is its codec. Since empty variants do not have data to encode, they simply use the unit codec — documented below — which does nothing.
It requires the discriminator and codec of each variant as a first argument. Similarly to the struct codec, these are defined as an array of variant tuples where the first item is the discriminator of the variant and the second item is its codec. Since empty variants do not have data to encode, they simply use the unit codec — documented below — which does nothing.

Here is how we can create a data enum codec for our previous example.

Expand All @@ -274,12 +274,12 @@ const messageCodec = getDataEnumCodec([
['Quit', getUnitCodec()],

// Tuple variant.
['Write', getStructCodec<{ fields: [string] }>([['fields', getTupleCodec([getStringCodec()])]])],
['Write', getStructCodec([['fields', getTupleCodec([getStringCodec()])]])],

// Struct variant.
[
'Move',
getStructCodec<{ x: number; y: number }>([
getStructCodec([
['x', getI32Codec()],
['y', getI32Codec()],
]),
Expand Down Expand Up @@ -327,7 +327,19 @@ u32MessageCodec.encode({ __kind: 'Move', x: 5, y: 6 });
// └------┘ 4-byte discriminator (Index 2).
```

Separate `getDataEnumEncoder` and `getDataEnumDecoder` functions are also available.
You may also customize the discriminator property — which defaults to `__kind` — by providing the desired property name as the `discriminator` option like so:

```ts
const messageCodec = getDataEnumCodec([...], {
discriminator: 'message',
});

messageCodec.encode({ message: 'Quit' });
messageCodec.encode({ message: 'Write', fields: ['Hi'] });
messageCodec.encode({ message: 'Move', x: 5, y: 6 });
```

Finally, note that separate `getDataEnumEncoder` and `getDataEnumDecoder` functions are available.

```ts
const bytes = getDataEnumEncoder(variantEncoders).encode({ __kind: 'Quit' });
Expand Down
30 changes: 22 additions & 8 deletions packages/codecs-data-structures/src/__tests__/data-enum-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,30 @@ describe('getDataEnumCodec', () => {
});

it('encodes data enums with different From and To types', () => {
const x = dataEnum(getU64Enum());
expect(x.encode({ __kind: 'B', value: 2 })).toStrictEqual(b('010200000000000000'));
expect(x.encode({ __kind: 'B', value: 2n })).toStrictEqual(b('010200000000000000'));
expect(x.read(b('010200000000000000'), 0)).toStrictEqual([{ __kind: 'B', value: 2n }, 9]);
const codec = dataEnum(getU64Enum());
expect(codec.encode({ __kind: 'B', value: 2 })).toStrictEqual(b('010200000000000000'));
expect(codec.encode({ __kind: 'B', value: 2n })).toStrictEqual(b('010200000000000000'));
expect(codec.read(b('010200000000000000'), 0)).toStrictEqual([{ __kind: 'B', value: 2n }, 9]);
});

it('encodes data enums with custom prefix', () => {
const x = dataEnum(getSameSizeVariants(), { size: u32() });
expect(x.encode({ __kind: 'A', value: 42 })).toStrictEqual(b('000000002a00'));
expect(x.read(b('000000002a00'), 0)).toStrictEqual([{ __kind: 'A', value: 42 }, 6]);
it('encodes data enums with a custom prefix', () => {
const codec = dataEnum(getSameSizeVariants(), { size: u32() });
expect(codec.encode({ __kind: 'A', value: 42 })).toStrictEqual(b('000000002a00'));
expect(codec.read(b('000000002a00'), 0)).toStrictEqual([{ __kind: 'A', value: 42 }, 6]);
});

it('encodes data enums with a custom discriminator property', () => {
const codec = dataEnum(
[
['small', struct([['value', u8()]])],
['large', struct([['value', u32()]])],
],
{ discriminator: 'size' },
);
expect(codec.encode({ size: 'small', value: 42 })).toStrictEqual(b('002a'));
expect(codec.read(b('002a'), 0)).toStrictEqual([{ size: 'small', value: 42 }, 2]);
expect(codec.encode({ size: 'large', value: 42 })).toStrictEqual(b('012a000000'));
expect(codec.read(b('012a000000'), 0)).toStrictEqual([{ size: 'large', value: 42 }, 5]);
});

it('has the right sizes', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,103 @@ import { getDataEnumCodec, getDataEnumDecoder, getDataEnumEncoder } from '../dat
import { getStructCodec } from '../struct';
import { getUnitCodec } from '../unit';

// [DESCRIBE] getDataEnumEncoder.
{
// [getDataEnumEncoder]: It constructs data enums from a list of encoder variants.
getDataEnumEncoder([
['A', {} as Encoder<{ value: string }>],
['B', {} as Encoder<{ x: number; y: number }>],
]) satisfies Encoder<{ __kind: 'A'; value: string } | { __kind: 'B'; x: number; y: number }>;
}
// It constructs data enums from a list of encoder variants.
{
getDataEnumEncoder([
['A', {} as Encoder<{ value: string }>],
['B', {} as Encoder<{ x: number; y: number }>],
]) satisfies Encoder<{ __kind: 'A'; value: string } | { __kind: 'B'; x: number; y: number }>;
}

{
// [getDataEnumDecoder]: It constructs data enums from a list of decoder variants.
getDataEnumDecoder([
['A', {} as Decoder<{ value: string }>],
['B', {} as Decoder<{ x: number; y: number }>],
]) satisfies Decoder<{ __kind: 'A'; value: string } | { __kind: 'B'; x: number; y: number }>;
// It can use a custom discriminator property.
{
getDataEnumEncoder(
[
['A', {} as Encoder<{ value: string }>],
['B', {} as Encoder<{ x: number; y: number }>],
],
{ discriminator: 'myType' },
) satisfies Encoder<{ myType: 'A'; value: string } | { myType: 'B'; x: number; y: number }>;
}
}

// [DESCRIBE] getDataEnumDecoder.
{
// [getDataEnumCodec]: It constructs data enums from a list of codec variants.
getDataEnumCodec([
['A', {} as Codec<{ value: string }>],
['B', {} as Codec<{ x: number; y: number }>],
]) satisfies Codec<{ __kind: 'A'; value: string } | { __kind: 'B'; x: number; y: number }>;
}
// It constructs data enums from a list of decoder variants.
{
getDataEnumDecoder([
['A', {} as Decoder<{ value: string }>],
['B', {} as Decoder<{ x: number; y: number }>],
]) satisfies Decoder<{ __kind: 'A'; value: string } | { __kind: 'B'; x: number; y: number }>;
}

{
// [getDataEnumCodec]: It can infer complex data enum types from provided variants.
getDataEnumCodec([
['PageLoad', {} as Codec<void>],
[
'Click',
getStructCodec([
['x', {} as Codec<number>],
['y', {} as Codec<number>],
]),
],
['KeyPress', getStructCodec([['fields', {} as Codec<[string]>]])],
['PageUnload', {} as Codec<object>],
]) satisfies Codec<
| { __kind: 'Click'; x: number; y: number }
| { __kind: 'KeyPress'; fields: [string] }
| { __kind: 'PageLoad' }
| { __kind: 'PageUnload' }
>;
// It can use a custom discriminator property.
{
getDataEnumDecoder(
[
['A', {} as Decoder<{ value: string }>],
['B', {} as Decoder<{ x: number; y: number }>],
],
{ discriminator: 'myType' },
) satisfies Decoder<{ myType: 'A'; value: string } | { myType: 'B'; x: number; y: number }>;
}
}

// [DESCRIBE] getDataEnumCodec.
{
// [getDataEnumCodec]: It can infer codec data enum with different from and to types.
getDataEnumCodec([
['A', getUnitCodec()],
['B', getStructCodec([['value', getU64Codec()]])],
]) satisfies Codec<
{ __kind: 'A' } | { __kind: 'B'; value: bigint | number },
{ __kind: 'A' } | { __kind: 'B'; value: bigint }
>;
// It constructs data enums from a list of codec variants.
{
getDataEnumCodec([
['A', {} as Codec<{ value: string }>],
['B', {} as Codec<{ x: number; y: number }>],
]) satisfies Codec<{ __kind: 'A'; value: string } | { __kind: 'B'; x: number; y: number }>;
}

// It can use a custom discriminator property.
{
getDataEnumCodec(
[
['A', {} as Codec<{ value: string }>],
['B', {} as Codec<{ x: number; y: number }>],
],
{ discriminator: 'myType' },
) satisfies Codec<{ myType: 'A'; value: string } | { myType: 'B'; x: number; y: number }>;
}

// It can infer complex data enum types from provided variants.
{
getDataEnumCodec(
[
['PageLoad', {} as Codec<void>],
[
'Click',
getStructCodec([
['x', {} as Codec<number>],
['y', {} as Codec<number>],
]),
],
['KeyPress', getStructCodec([['fields', {} as Codec<[string]>]])],
['PageUnload', {} as Codec<object>],
],
{ discriminator: 'event' },
) satisfies Codec<
| { event: 'Click'; x: number; y: number }
| { event: 'KeyPress'; fields: [string] }
| { event: 'PageLoad' }
| { event: 'PageUnload' }
>;
}

// It can infer codec data enum with different from and to types.
{
getDataEnumCodec([
['A', getUnitCodec()],
['B', getStructCodec([['value', getU64Codec()]])],
]) satisfies Codec<
{ __kind: 'A' } | { __kind: 'B'; value: bigint | number },
{ __kind: 'A' } | { __kind: 'B'; value: bigint }
>;
}
}
Loading

0 comments on commit bf029dd

Please sign in to comment.