Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(experimental): support custom discriminator property for getDataEnumCodec #2380

Merged
merged 1 commit into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added via Giphy

{
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
Loading