Skip to content

Commit

Permalink
refactor(experimental): rename getDataEnumCodec to getDiscriminatedUn…
Browse files Browse the repository at this point in the history
…ionCodec
  • Loading branch information
lorisleiva committed Mar 26, 2024
1 parent 4f9a7da commit 4997976
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 130 deletions.
8 changes: 8 additions & 0 deletions .changeset/odd-beds-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@solana/codecs-data-structures': patch
'@solana/codecs-core': patch
'@solana/codecs': patch
'@solana/errors': patch
---

`getDataEnumCodec` is now called `getDiscriminatedUnionCodec`
2 changes: 1 addition & 1 deletion packages/codecs-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ There is a significant library of composable codecs at your disposal, enabling y

- [`@solana/codecs-numbers`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-numbers) for number codecs.
- [`@solana/codecs-strings`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-strings) for string codecs.
- [`@solana/codecs-data-structures`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures) for many data structure codecs such as objects, arrays, tuples, sets, maps, scalar enums, data enums, booleans, etc.
- [`@solana/codecs-data-structures`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures) for many data structure codecs such as objects, arrays, tuples, sets, maps, scalar enums, discriminated unions, booleans, etc.
- [`@solana/options`](https://github.com/solana-labs/solana-web3.js/tree/master/packages/options) for a Rust-like `Option` type and associated codec.

You may also be interested in some of the helpers of this `@solana/codecs-core` library such as `mapCodec`, `fixCodec` or `reverseCodec` that create new codecs from existing ones.
Expand Down
30 changes: 15 additions & 15 deletions packages/codecs-data-structures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,17 +243,17 @@ const bytes = getScalarEnumEncoder(Direction).encode(Direction.Left);
const direction = getScalarEnumDecoder(Direction).decode(bytes);
```

## Data enum codec
## Discriminated union codec

In Rust, enums are powerful data types whose variants can be one of the following:

- An empty variant — e.g. `enum Message { Quit }`.
- A tuple variant — e.g. `enum Message { Write(String) }`.
- A struct variant — e.g. `enum Message { Move { x: i32, y: i32 } }`.

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**.
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 discriminated union**.

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.
We use a special field named `__kind` to distinguish between the different variants of a discriminated union. 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 @@ -262,14 +262,14 @@ type Message =
| { __kind: 'Move'; x: number; y: number }; // Struct variant.
```

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

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.
Here is how we can create a discriminated union codec for our previous example.

```ts
const messageCodec = getDataEnumCodec([
const messageCodec = getDiscriminatedUnionCodec([
// Empty variant.
['Quit', getUnitCodec()],

Expand All @@ -287,7 +287,7 @@ const messageCodec = getDataEnumCodec([
]);
```

And here’s how we can use such a codec to encode data enums. Notice that by default, they use a `u8` number prefix to distinguish between the different types of variants.
And here’s how we can use such a codec to encode discriminated unions. Notice that by default, they use a `u8` number prefix to distinguish between the different types of variants.

```ts
messageCodec.encode({ __kind: 'Quit' });
Expand All @@ -307,10 +307,10 @@ messageCodec.encode({ __kind: 'Move', x: 5, y: 6 });
// └-- 1-byte discriminator (Index 2 — the "Move" variant).
```

However, you may provide a number codec as the `size` option of the `getDataEnumCodec` function to customise that behaviour.
However, you may provide a number codec as the `size` option of the `getDiscriminatedUnionCodec` function to customise that behaviour.

```ts
const u32MessageCodec = getDataEnumCodec([...], {
const u32MessageCodec = getDiscriminatedUnionCodec([...], {
size: getU32Codec(),
});

Expand All @@ -330,7 +330,7 @@ u32MessageCodec.encode({ __kind: 'Move', x: 5, y: 6 });
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([...], {
const messageCodec = getDiscriminatedUnionCodec([...], {
discriminator: 'message',
});

Expand All @@ -347,7 +347,7 @@ enum Message {
Write,
Move,
}
const messageCodec = getDataEnumCodec([
const messageCodec = getDiscriminatedUnionCodec([
[Message.Quit, getUnitCodec()],
[Message.Write, getStructCodec([...])],
[Message.Move, getStructCodec([...])],
Expand All @@ -358,11 +358,11 @@ codec.encode({ __kind: Message.Write, fields: ['Hi'] });
codec.encode({ __kind: Message.Move, x: 5, y: 6 });
```

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

```ts
const bytes = getDataEnumEncoder(variantEncoders).encode({ __kind: 'Quit' });
const message = getDataEnumDecoder(variantDecoders).decode(bytes);
const bytes = getDiscriminatedUnionEncoder(variantEncoders).encode({ __kind: 'Quit' });
const message = getDiscriminatedUnionDecoder(variantDecoders).decode(bytes);
```

## Boolean codec
Expand Down Expand Up @@ -514,7 +514,7 @@ const decodedBooleans = getBitArrayDecoder(1).decode(bytes);

## Unit codec

The `getUnitCodec` function returns a `Codec<void>` that encodes `undefined` into an empty `Uint8Array` and returns `undefined` without consuming any bytes when decoding. This is more of a low-level codec that can be used internally by other codecs. For instance, this is how data enum codecs describe the codecs of empty variants.
The `getUnitCodec` function returns a `Codec<void>` that encodes `undefined` into an empty `Uint8Array` and returns `undefined` without consuming any bytes when decoding. This is more of a low-level codec that can be used internally by other codecs. For instance, this is how discriminated union codecs describe the codecs of empty variants.

```ts
getUnitCodec().encode(undefined); // Empty Uint8Array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ import { getU8Codec, getU16Codec, getU32Codec, getU64Codec } from '@solana/codec
import { getStringCodec } from '@solana/codecs-strings';
import {
SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE,
SOLANA_ERROR__CODECS__INVALID_DATA_ENUM_VARIANT,
SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT,
SolanaError,
} from '@solana/errors';

import { getArrayCodec } from '../array';
import { getBooleanCodec } from '../boolean';
import { getDataEnumCodec } from '../data-enum';
import { getDiscriminatedUnionCodec } from '../discriminated-union';
import { getStructCodec } from '../struct';
import { getTupleCodec } from '../tuple';
import { getUnitCodec } from '../unit';
import { b } from './__setup__';

describe('getDataEnumCodec', () => {
const dataEnum = getDataEnumCodec;
describe('getDiscriminatedUnionCodec', () => {
const discriminatedUnion = getDiscriminatedUnionCodec;
const array = getArrayCodec;
const boolean = getBooleanCodec;
const string = getStringCodec;
Expand Down Expand Up @@ -76,45 +76,45 @@ describe('getDataEnumCodec', () => {

it('encodes empty variants', () => {
const pageLoad: WebEvent = { __kind: 'PageLoad' };
expect(dataEnum(getWebEvent()).encode(pageLoad)).toStrictEqual(b('00'));
expect(dataEnum(getWebEvent()).read(b('00'), 0)).toStrictEqual([pageLoad, 1]);
expect(dataEnum(getWebEvent()).read(b('ffff00'), 2)).toStrictEqual([pageLoad, 3]);
expect(discriminatedUnion(getWebEvent()).encode(pageLoad)).toStrictEqual(b('00'));
expect(discriminatedUnion(getWebEvent()).read(b('00'), 0)).toStrictEqual([pageLoad, 1]);
expect(discriminatedUnion(getWebEvent()).read(b('ffff00'), 2)).toStrictEqual([pageLoad, 3]);
const pageUnload: WebEvent = { __kind: 'PageUnload' };
expect(dataEnum(getWebEvent()).encode(pageUnload)).toStrictEqual(b('03'));
expect(dataEnum(getWebEvent()).read(b('03'), 0)).toStrictEqual([pageUnload, 1]);
expect(dataEnum(getWebEvent()).read(b('ffff03'), 2)).toStrictEqual([pageUnload, 3]);
expect(discriminatedUnion(getWebEvent()).encode(pageUnload)).toStrictEqual(b('03'));
expect(discriminatedUnion(getWebEvent()).read(b('03'), 0)).toStrictEqual([pageUnload, 1]);
expect(discriminatedUnion(getWebEvent()).read(b('ffff03'), 2)).toStrictEqual([pageUnload, 3]);
});

it('encodes struct variants', () => {
const click = (x: number, y: number): WebEvent => ({ __kind: 'Click', x, y });
expect(dataEnum(getWebEvent()).encode(click(0, 0))).toStrictEqual(b('010000'));
expect(dataEnum(getWebEvent()).read(b('010000'), 0)).toStrictEqual([click(0, 0), 3]);
expect(dataEnum(getWebEvent()).read(b('ffff010000'), 2)).toStrictEqual([click(0, 0), 5]);
expect(dataEnum(getWebEvent()).encode(click(1, 2))).toStrictEqual(b('010102'));
expect(dataEnum(getWebEvent()).read(b('010102'), 0)).toStrictEqual([click(1, 2), 3]);
expect(dataEnum(getWebEvent()).read(b('ffff010102'), 2)).toStrictEqual([click(1, 2), 5]);
expect(discriminatedUnion(getWebEvent()).encode(click(0, 0))).toStrictEqual(b('010000'));
expect(discriminatedUnion(getWebEvent()).read(b('010000'), 0)).toStrictEqual([click(0, 0), 3]);
expect(discriminatedUnion(getWebEvent()).read(b('ffff010000'), 2)).toStrictEqual([click(0, 0), 5]);
expect(discriminatedUnion(getWebEvent()).encode(click(1, 2))).toStrictEqual(b('010102'));
expect(discriminatedUnion(getWebEvent()).read(b('010102'), 0)).toStrictEqual([click(1, 2), 3]);
expect(discriminatedUnion(getWebEvent()).read(b('ffff010102'), 2)).toStrictEqual([click(1, 2), 5]);
});

it('encodes tuple variants', () => {
const press = (k: string): WebEvent => ({ __kind: 'KeyPress', fields: [k] });
expect(dataEnum(getWebEvent()).encode(press(''))).toStrictEqual(b('0200000000'));
expect(dataEnum(getWebEvent()).read(b('0200000000'), 0)).toStrictEqual([press(''), 5]);
expect(dataEnum(getWebEvent()).read(b('ffff0200000000'), 2)).toStrictEqual([press(''), 7]);
expect(dataEnum(getWebEvent()).encode(press('1'))).toStrictEqual(b('020100000031'));
expect(dataEnum(getWebEvent()).read(b('020100000031'), 0)).toStrictEqual([press('1'), 6]);
expect(dataEnum(getWebEvent()).read(b('ffff020100000031'), 2)).toStrictEqual([press('1'), 8]);
expect(dataEnum(getWebEvent()).encode(press('語'))).toStrictEqual(b('0203000000e8aa9e'));
expect(dataEnum(getWebEvent()).encode(press('enter'))).toStrictEqual(b('0205000000656e746572'));
expect(discriminatedUnion(getWebEvent()).encode(press(''))).toStrictEqual(b('0200000000'));
expect(discriminatedUnion(getWebEvent()).read(b('0200000000'), 0)).toStrictEqual([press(''), 5]);
expect(discriminatedUnion(getWebEvent()).read(b('ffff0200000000'), 2)).toStrictEqual([press(''), 7]);
expect(discriminatedUnion(getWebEvent()).encode(press('1'))).toStrictEqual(b('020100000031'));
expect(discriminatedUnion(getWebEvent()).read(b('020100000031'), 0)).toStrictEqual([press('1'), 6]);
expect(discriminatedUnion(getWebEvent()).read(b('ffff020100000031'), 2)).toStrictEqual([press('1'), 8]);
expect(discriminatedUnion(getWebEvent()).encode(press('語'))).toStrictEqual(b('0203000000e8aa9e'));
expect(discriminatedUnion(getWebEvent()).encode(press('enter'))).toStrictEqual(b('0205000000656e746572'));
});

it('handles invalid variants', () => {
expect(() => dataEnum(getWebEvent()).encode({ __kind: 'Missing' } as unknown as WebEvent)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__INVALID_DATA_ENUM_VARIANT, {
expect(() => discriminatedUnion(getWebEvent()).encode({ __kind: 'Missing' } as unknown as WebEvent)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT, {
value: 'Missing',
variants: ['PageLoad', 'Click', 'KeyPress', 'PageUnload'],
}),
);
expect(() => dataEnum(getWebEvent()).read(new Uint8Array([4]), 0)).toThrow(
expect(() => discriminatedUnion(getWebEvent()).read(new Uint8Array([4]), 0)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, {
discriminator: 4,
maxRange: 3,
Expand All @@ -123,21 +123,21 @@ describe('getDataEnumCodec', () => {
);
});

it('encodes data enums with different From and To types', () => {
const codec = dataEnum(getU64Enum());
it('encodes discriminated unions with different From and To types', () => {
const codec = discriminatedUnion(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 a custom prefix', () => {
const codec = dataEnum(getSameSizeVariants(), { size: u32() });
it('encodes discriminated unions with a custom prefix', () => {
const codec = discriminatedUnion(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(
it('encodes discriminated unions with a custom discriminator property', () => {
const codec = discriminatedUnion(
[
['small', struct([['value', u8()]])],
['large', struct([['value', u32()]])],
Expand All @@ -150,21 +150,21 @@ describe('getDataEnumCodec', () => {
expect(codec.read(b('012a000000'), 0)).toStrictEqual([{ size: 'large', value: 42 }, 5]);
});

it('encodes data enums with number discriminator values', () => {
const codec = dataEnum([
it('encodes discriminated unions with number discriminator values', () => {
const codec = discriminatedUnion([
[1, struct([['one', u8()]])],
[2, struct([['two', u32()]])],
]);
expect(codec.encode({ __kind: 1, one: 42 })).toStrictEqual(b('002a'));
expect(codec.read(b('002a'), 0)).toStrictEqual([{ __kind: 1, one: 42 }, 2]);
});

it('encodes data enums with enum discriminator values', () => {
it('encodes discriminated unions with enum discriminator values', () => {
enum Event {
Click,
KeyPress,
}
const codec = dataEnum([
const codec = discriminatedUnion([
[
Event.Click,
struct([
Expand All @@ -179,7 +179,7 @@ describe('getDataEnumCodec', () => {
});

it('has the right sizes', () => {
const webEvent = dataEnum(getWebEvent());
const webEvent = discriminatedUnion(getWebEvent());
expect(isVariableSize(webEvent)).toBe(true);
assertIsVariableSize(webEvent);
expect(webEvent.getSizeFromValue({ __kind: 'PageLoad' })).toBe(1);
Expand All @@ -188,28 +188,28 @@ describe('getDataEnumCodec', () => {
expect(webEvent.getSizeFromValue({ __kind: 'KeyPress', fields: ['ABC'] })).toBe(1 + 4 + 3);
expect(webEvent.maxSize).toBeUndefined();

const sameSize = dataEnum(getSameSizeVariants());
const sameSize = discriminatedUnion(getSameSizeVariants());
expect(isFixedSize(sameSize)).toBe(true);
assertIsFixedSize(sameSize);
expect(sameSize.fixedSize).toBe(3);

const sameSizeU32 = dataEnum(getSameSizeVariants(), { size: u32() });
const sameSizeU32 = discriminatedUnion(getSameSizeVariants(), { size: u32() });
expect(isFixedSize(sameSizeU32)).toBe(true);
assertIsFixedSize(sameSizeU32);
expect(sameSizeU32.fixedSize).toBe(6);

const u64Enum = dataEnum(getU64Enum());
const u64Enum = discriminatedUnion(getU64Enum());
expect(isVariableSize(u64Enum)).toBe(true);
assertIsVariableSize(u64Enum);
expect(u64Enum.maxSize).toBe(9);
});

it('offsets variants within a data enum', () => {
it('offsets variants within a discriminated union', () => {
const offsettedU32 = offsetCodec(
resizeCodec(u32(), size => size + 2),
{ preOffset: ({ preOffset }) => preOffset + 2 },
);
const codec = dataEnum([
const codec = discriminatedUnion([
['Simple', struct([['n', u32()]])],
['WithOffset', struct([['n', offsettedU32]])],
]);
Expand Down
Loading

0 comments on commit 4997976

Please sign in to comment.