From 360fa618d9e2c2674c12434d879608963fe4d128 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 26 Mar 2024 15:52:08 +0000 Subject: [PATCH] refactor(experimental): rename getDataEnumCodec to getDiscriminatedUnionCodec --- .changeset/odd-beds-punch.md | 8 ++ packages/codecs-core/README.md | 2 +- packages/codecs-data-structures/README.md | 30 +++---- ...um-test.ts => discriminated-union-test.ts} | 86 +++++++++--------- ...est.ts => discriminated-union-typetest.ts} | 50 ++++++----- .../{data-enum.ts => discriminated-union.ts} | 90 +++++++++++-------- packages/codecs-data-structures/src/index.ts | 2 +- packages/codecs/README.md | 2 +- packages/errors/src/codes.ts | 4 +- packages/errors/src/context.ts | 4 +- packages/errors/src/messages.ts | 6 +- 11 files changed, 154 insertions(+), 130 deletions(-) create mode 100644 .changeset/odd-beds-punch.md rename packages/codecs-data-structures/src/__tests__/{data-enum-test.ts => discriminated-union-test.ts} (64%) rename packages/codecs-data-structures/src/__typetests__/{data-enum-typetest.ts => discriminated-union-typetest.ts} (80%) rename packages/codecs-data-structures/src/{data-enum.ts => discriminated-union.ts} (70%) diff --git a/.changeset/odd-beds-punch.md b/.changeset/odd-beds-punch.md new file mode 100644 index 000000000000..55d8bc16a720 --- /dev/null +++ b/.changeset/odd-beds-punch.md @@ -0,0 +1,8 @@ +--- +'@solana/codecs-data-structures': patch +'@solana/codecs-core': patch +'@solana/codecs': patch +'@solana/errors': patch +--- + +`getDataEnumCodec` is now called `getDiscriminatedUnionCodec` diff --git a/packages/codecs-core/README.md b/packages/codecs-core/README.md index 9ea7845e00cd..bf613a29626b 100644 --- a/packages/codecs-core/README.md +++ b/packages/codecs-core/README.md @@ -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. diff --git a/packages/codecs-data-structures/README.md b/packages/codecs-data-structures/README.md index 06ba5d126b07..22c6bb8bc5c0 100644 --- a/packages/codecs-data-structures/README.md +++ b/packages/codecs-data-structures/README.md @@ -243,7 +243,7 @@ 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: @@ -251,9 +251,9 @@ In Rust, enums are powerful data types whose variants can be one of the followin - 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 = @@ -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()], @@ -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' }); @@ -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(), }); @@ -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', }); @@ -347,7 +347,7 @@ enum Message { Write, Move, } -const messageCodec = getDataEnumCodec([ +const messageCodec = getDiscriminatedUnionCodec([ [Message.Quit, getUnitCodec()], [Message.Write, getStructCodec([...])], [Message.Move, getStructCodec([...])], @@ -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 @@ -514,7 +514,7 @@ const decodedBooleans = getBitArrayDecoder(1).decode(bytes); ## Unit codec -The `getUnitCodec` function returns a `Codec` 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` 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 diff --git a/packages/codecs-data-structures/src/__tests__/data-enum-test.ts b/packages/codecs-data-structures/src/__tests__/discriminated-union-test.ts similarity index 64% rename from packages/codecs-data-structures/src/__tests__/data-enum-test.ts rename to packages/codecs-data-structures/src/__tests__/discriminated-union-test.ts index 8c737877aa50..6a4e7520c1e2 100644 --- a/packages/codecs-data-structures/src/__tests__/data-enum-test.ts +++ b/packages/codecs-data-structures/src/__tests__/discriminated-union-test.ts @@ -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; @@ -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, @@ -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()]])], @@ -150,8 +150,8 @@ 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()]])], ]); @@ -159,12 +159,12 @@ describe('getDataEnumCodec', () => { 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([ @@ -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); @@ -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]])], ]); diff --git a/packages/codecs-data-structures/src/__typetests__/data-enum-typetest.ts b/packages/codecs-data-structures/src/__typetests__/discriminated-union-typetest.ts similarity index 80% rename from packages/codecs-data-structures/src/__typetests__/data-enum-typetest.ts rename to packages/codecs-data-structures/src/__typetests__/discriminated-union-typetest.ts index 5c308073f429..ea995d147949 100644 --- a/packages/codecs-data-structures/src/__typetests__/data-enum-typetest.ts +++ b/packages/codecs-data-structures/src/__typetests__/discriminated-union-typetest.ts @@ -1,15 +1,19 @@ import { Codec, Decoder, Encoder } from '@solana/codecs-core'; import { getU64Codec } from '@solana/codecs-numbers'; -import { getDataEnumCodec, getDataEnumDecoder, getDataEnumEncoder } from '../data-enum'; +import { + getDiscriminatedUnionCodec, + getDiscriminatedUnionDecoder, + getDiscriminatedUnionEncoder, +} from '../discriminated-union'; import { getStructCodec } from '../struct'; import { getUnitCodec } from '../unit'; -// [DESCRIBE] getDataEnumEncoder. +// [DESCRIBE] getDiscriminatedUnionEncoder. { - // It constructs data enums from a list of encoder variants. + // It constructs discriminated unions from a list of encoder variants. { - getDataEnumEncoder([ + getDiscriminatedUnionEncoder([ ['A', {} as Encoder<{ value: string }>], ['B', {} as Encoder<{ x: number; y: number }>], ]) satisfies Encoder<{ __kind: 'A'; value: string } | { __kind: 'B'; x: number; y: number }>; @@ -17,7 +21,7 @@ import { getUnitCodec } from '../unit'; // It can use a custom discriminator property. { - getDataEnumEncoder( + getDiscriminatedUnionEncoder( [ ['A', {} as Encoder<{ value: string }>], ['B', {} as Encoder<{ x: number; y: number }>], @@ -28,7 +32,7 @@ import { getUnitCodec } from '../unit'; // It can use numbers as discriminator values. { - getDataEnumEncoder([ + getDiscriminatedUnionEncoder([ [1, {} as Encoder<{ value: string }>], [2, {} as Encoder<{ x: number; y: number }>], ]) satisfies Encoder<{ __kind: 1; value: string } | { __kind: 2; x: number; y: number }>; @@ -40,18 +44,18 @@ import { getUnitCodec } from '../unit'; Click, KeyPress, } - getDataEnumEncoder([ + getDiscriminatedUnionEncoder([ [Event.Click, {} as Encoder<{ x: number; y: number }>], [Event.KeyPress, {} as Encoder<{ key: string }>], ]) satisfies Encoder<{ __kind: Event.Click; x: number; y: number } | { __kind: Event.KeyPress; key: string }>; } } -// [DESCRIBE] getDataEnumDecoder. +// [DESCRIBE] getDiscriminatedUnionDecoder. { - // It constructs data enums from a list of decoder variants. + // It constructs discriminated unions from a list of decoder variants. { - getDataEnumDecoder([ + getDiscriminatedUnionDecoder([ ['A', {} as Decoder<{ value: string }>], ['B', {} as Decoder<{ x: number; y: number }>], ]) satisfies Decoder<{ __kind: 'A'; value: string } | { __kind: 'B'; x: number; y: number }>; @@ -59,7 +63,7 @@ import { getUnitCodec } from '../unit'; // It can use a custom discriminator property. { - getDataEnumDecoder( + getDiscriminatedUnionDecoder( [ ['A', {} as Decoder<{ value: string }>], ['B', {} as Decoder<{ x: number; y: number }>], @@ -70,7 +74,7 @@ import { getUnitCodec } from '../unit'; // It can use numbers as discriminator values. { - getDataEnumDecoder([ + getDiscriminatedUnionDecoder([ [1, {} as Decoder<{ value: string }>], [2, {} as Decoder<{ x: number; y: number }>], ]) satisfies Decoder<{ __kind: 1; value: string } | { __kind: 2; x: number; y: number }>; @@ -82,18 +86,18 @@ import { getUnitCodec } from '../unit'; Click, KeyPress, } - getDataEnumDecoder([ + getDiscriminatedUnionDecoder([ [Event.Click, {} as Decoder<{ x: number; y: number }>], [Event.KeyPress, {} as Decoder<{ key: string }>], ]) satisfies Decoder<{ __kind: Event.Click; x: number; y: number } | { __kind: Event.KeyPress; key: string }>; } } -// [DESCRIBE] getDataEnumCodec. +// [DESCRIBE] getDiscriminatedUnionCodec. { - // It constructs data enums from a list of codec variants. + // It constructs discriminated unions from a list of codec variants. { - getDataEnumCodec([ + getDiscriminatedUnionCodec([ ['A', {} as Codec<{ value: string }>], ['B', {} as Codec<{ x: number; y: number }>], ]) satisfies Codec<{ __kind: 'A'; value: string } | { __kind: 'B'; x: number; y: number }>; @@ -101,7 +105,7 @@ import { getUnitCodec } from '../unit'; // It can use a custom discriminator property. { - getDataEnumCodec( + getDiscriminatedUnionCodec( [ ['A', {} as Codec<{ value: string }>], ['B', {} as Codec<{ x: number; y: number }>], @@ -112,7 +116,7 @@ import { getUnitCodec } from '../unit'; // It can use numbers as discriminator values. { - getDataEnumCodec([ + getDiscriminatedUnionCodec([ [1, {} as Codec<{ value: string }>], [2, {} as Codec<{ x: number; y: number }>], ]) satisfies Codec<{ __kind: 1; value: string } | { __kind: 2; x: number; y: number }>; @@ -124,15 +128,15 @@ import { getUnitCodec } from '../unit'; Click, KeyPress, } - getDataEnumCodec([ + getDiscriminatedUnionCodec([ [Event.Click, {} as Codec<{ x: number; y: number }>], [Event.KeyPress, {} as Codec<{ key: string }>], ]) satisfies Codec<{ __kind: Event.Click; x: number; y: number } | { __kind: Event.KeyPress; key: string }>; } - // It can infer complex data enum types from provided variants. + // It can infer complex discriminated union types from provided variants. { - getDataEnumCodec( + getDiscriminatedUnionCodec( [ ['PageLoad', {} as Codec], [ @@ -154,9 +158,9 @@ import { getUnitCodec } from '../unit'; >; } - // It can infer codec data enum with different from and to types. + // It can infer codec discriminated union with different from and to types. { - getDataEnumCodec([ + getDiscriminatedUnionCodec([ ['A', getUnitCodec()], ['B', getStructCodec([['value', getU64Codec()]])], ]) satisfies Codec< diff --git a/packages/codecs-data-structures/src/data-enum.ts b/packages/codecs-data-structures/src/discriminated-union.ts similarity index 70% rename from packages/codecs-data-structures/src/data-enum.ts rename to packages/codecs-data-structures/src/discriminated-union.ts index 5e36ba814ace..3c7b96a609d0 100644 --- a/packages/codecs-data-structures/src/data-enum.ts +++ b/packages/codecs-data-structures/src/discriminated-union.ts @@ -13,14 +13,14 @@ import { import { getU8Decoder, getU8Encoder, NumberCodec, NumberDecoder, NumberEncoder } from '@solana/codecs-numbers'; 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 { DrainOuterGeneric, getMaxSize, maxCodecSizes, sumCodecSizes } from './utils'; /** - * Defines a data enum using discriminated union types. + * Defines a discriminated union using discriminated union types. * * @example * ```ts @@ -29,48 +29,51 @@ import { DrainOuterGeneric, getMaxSize, maxCodecSizes, sumCodecSizes } from './u * | { __kind: 'click', x: number, y: number }; * ``` */ -export type DataEnum = { +export type DiscriminatedUnion< + TDiscriminatorProperty extends string = '__kind', + TDiscriminatorValue extends string = string, +> = { [P in TDiscriminatorProperty]: TDiscriminatorValue; }; /** - * Extracts a variant from a data enum. + * Extracts a variant from a discriminated union. * * @example * ```ts * type WebPageEvent = * | { __kind: 'pageview', url: string } * | { __kind: 'click', x: number, y: number }; - * type ClickEvent = GetDataEnumKind; + * type ClickEvent = GetDiscriminatedUnionVariant; * // -> { __kind: 'click', x: number, y: number } * ``` */ -export type GetDataEnumKind< - TDataEnum extends DataEnum, +export type GetDiscriminatedUnionVariant< + TUnion extends DiscriminatedUnion, TDiscriminatorProperty extends string, - TDiscriminatorValue extends TDataEnum[TDiscriminatorProperty], -> = Extract>; + TDiscriminatorValue extends TUnion[TDiscriminatorProperty], +> = Extract>; /** - * Extracts a variant from a data enum without its discriminator. + * Extracts a variant from a discriminated union without its discriminator. * * @example * ```ts * type WebPageEvent = * | { __kind: 'pageview', url: string } * | { __kind: 'click', x: number, y: number }; - * type ClickEvent = GetDataEnumKindContent; + * type ClickEvent = GetDiscriminatedUnionVariantContent; * // -> { x: number, y: number } * ``` */ -export type GetDataEnumKindContent< - TDataEnum extends DataEnum, +export type GetDiscriminatedUnionVariantContent< + TUnion extends DiscriminatedUnion, TDiscriminatorProperty extends string, - TDiscriminatorValue extends TDataEnum[TDiscriminatorProperty], -> = Omit, TDiscriminatorProperty>; + TDiscriminatorValue extends TUnion[TDiscriminatorProperty], +> = Omit, TDiscriminatorProperty>; -/** Defines the config for data enum codecs. */ -export type DataEnumCodecConfig< +/** Defines the config for discriminated union codecs. */ +export type DiscriminatedUnionCodecConfig< TDiscriminatorProperty extends string = '__kind', TDiscriminatorSize = NumberCodec | NumberDecoder | NumberEncoder, > = { @@ -113,22 +116,22 @@ type GetDecoderTypeFromVariants< }>[ArrayIndices]; /** - * Creates a data enum encoder. + * Creates a discriminated union encoder. * - * @param variants - The variant encoders of the data enum. + * @param variants - The variant encoders of the discriminated union. * @param config - A set of config for the encoder. */ -export function getDataEnumEncoder< +export function getDiscriminatedUnionEncoder< const TVariants extends Variants>, const TDiscriminatorProperty extends string = '__kind', >( variants: TVariants, - config: DataEnumCodecConfig = {}, + config: DiscriminatedUnionCodecConfig = {}, ): Encoder> { type TFrom = GetEncoderTypeFromVariants; const discriminatorProperty = (config.discriminator ?? '__kind') as TDiscriminatorProperty; const prefix = config.size ?? getU8Encoder(); - const fixedSize = getDataEnumFixedSize(variants, prefix); + const fixedSize = getDiscriminatedUnionFixedSize(variants, prefix); return createEncoder({ ...(fixedSize !== null ? { fixedSize } @@ -141,7 +144,7 @@ export function getDataEnumEncoder< getEncodedSize(variant as TFrom & void, variantEncoder) ); }, - maxSize: getDataEnumMaxSize(variants, prefix), + maxSize: getDiscriminatedUnionMaxSize(variants, prefix), }), write: (variant: TFrom, bytes, offset) => { const discriminator = getVariantDiscriminator(variants, variant[discriminatorProperty]); @@ -153,26 +156,26 @@ export function getDataEnumEncoder< } /** - * Creates a data enum decoder. + * Creates a discriminated union decoder. * - * @param variants - The variant decoders of the data enum. + * @param variants - The variant decoders of the discriminated union. * @param config - A set of config for the decoder. */ -export function getDataEnumDecoder< +export function getDiscriminatedUnionDecoder< const TVariants extends Variants>, const TDiscriminatorProperty extends string = '__kind', >( variants: TVariants, - config: DataEnumCodecConfig = {}, + config: DiscriminatedUnionCodecConfig = {}, ): Decoder> { type TTo = GetDecoderTypeFromVariants; const discriminatorProperty = config.discriminator ?? '__kind'; const prefix = config.size ?? getU8Decoder(); - const fixedSize = getDataEnumFixedSize(variants, prefix); + const fixedSize = getDiscriminatedUnionFixedSize(variants, prefix); return createDecoder({ - ...(fixedSize !== null ? { fixedSize } : { maxSize: getDataEnumMaxSize(variants, prefix) }), + ...(fixedSize !== null ? { fixedSize } : { maxSize: getDiscriminatedUnionMaxSize(variants, prefix) }), read: (bytes: Uint8Array, offset) => { - assertByteArrayIsNotEmptyForCodec('dataEnum', bytes, offset); + assertByteArrayIsNotEmptyForCodec('discriminatedUnion', bytes, offset); const [discriminator, dOffset] = prefix.read(bytes, offset); offset = dOffset; const variantField = variants[Number(discriminator)] ?? null; @@ -191,32 +194,32 @@ export function getDataEnumDecoder< } /** - * Creates a data enum codec. + * Creates a discriminated union codec. * - * @param variants - The variant codecs of the data enum. + * @param variants - The variant codecs of the discriminated union. * @param config - A set of config for the codec. */ -export function getDataEnumCodec< +export function getDiscriminatedUnionCodec< const TVariants extends Variants>, const TDiscriminatorProperty extends string = '__kind', >( variants: TVariants, - config: DataEnumCodecConfig = {}, + config: DiscriminatedUnionCodecConfig = {}, ): Codec< GetEncoderTypeFromVariants, GetDecoderTypeFromVariants & GetEncoderTypeFromVariants > { return combineCodec( - getDataEnumEncoder(variants, config), - getDataEnumDecoder(variants, config) as Decoder< + getDiscriminatedUnionEncoder(variants, config), + getDiscriminatedUnionDecoder(variants, config) as Decoder< GetDecoderTypeFromVariants & GetEncoderTypeFromVariants >, ); } -function getDataEnumFixedSize | Encoder>>( +function getDiscriminatedUnionFixedSize | Encoder>>( variants: TVariants, prefix: object | { fixedSize: number }, ): number | null { @@ -230,7 +233,7 @@ function getDataEnumFixedSize | En return isFixedSize(prefix) ? prefix.fixedSize + variantSize : null; } -function getDataEnumMaxSize | Encoder>>( +function getDiscriminatedUnionMaxSize | Encoder>>( variants: TVariants, prefix: object | { fixedSize: number }, ) { @@ -244,10 +247,19 @@ function getVariantDiscriminator | ) { const discriminator = variants.findIndex(([key]) => discriminatorValue === key); if (discriminator < 0) { - throw new SolanaError(SOLANA_ERROR__CODECS__INVALID_DATA_ENUM_VARIANT, { + throw new SolanaError(SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT, { value: discriminatorValue, variants: variants.map(([key]) => key), }); } return discriminator; } + +/** @deprecated Use `getDiscriminatedUnionEncoder` instead. */ +export const getDataEnumEncoder = getDiscriminatedUnionEncoder; + +/** @deprecated Use `getDiscriminatedUnionDecoder` instead. */ +export const getDataEnumDecoder = getDiscriminatedUnionDecoder; + +/** @deprecated Use `getDiscriminatedUnionCodec` instead. */ +export const getDataEnumCodec = getDiscriminatedUnionCodec; diff --git a/packages/codecs-data-structures/src/index.ts b/packages/codecs-data-structures/src/index.ts index 89cec4340acd..22a9155d1c69 100644 --- a/packages/codecs-data-structures/src/index.ts +++ b/packages/codecs-data-structures/src/index.ts @@ -3,7 +3,7 @@ export * from './assertions'; export * from './bit-array'; export * from './boolean'; export * from './bytes'; -export * from './data-enum'; +export * from './discriminated-union'; export * from './map'; export * from './nullable'; export * from './scalar-enum'; diff --git a/packages/codecs/README.md b/packages/codecs/README.md index df5814d5f6e6..64af20aea116 100644 --- a/packages/codecs/README.md +++ b/packages/codecs/README.md @@ -82,7 +82,7 @@ The `@solana/codecs` package is composed of several smaller packages, each with - [Tuple codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#tuple-codec). - [Struct codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#struct-codec). - [Scalar enum codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#scalar-enum-codec). - - [Data enum codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#data-enum-codec). + - [Discriminated union codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#discriminated-union-codec). - [Boolean codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#boolean-codec). - [Nullable codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#nullable-codec). - [Bytes codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#bytes-codec). diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 5bac1bfeaafa..a622fcc0bc50 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -253,7 +253,7 @@ export const SOLANA_ERROR__CODECS__ENCODER_DECODER_FIXED_SIZE_MISMATCH = 8078005 export const SOLANA_ERROR__CODECS__ENCODER_DECODER_MAX_SIZE_MISMATCH = 8078006 as const; export const SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS = 8078007 as const; export const SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE = 8078008 as const; -export const SOLANA_ERROR__CODECS__INVALID_DATA_ENUM_VARIANT = 8078009 as const; +export const SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT = 8078009 as const; export const SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT = 8078010 as const; export const SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE = 8078011 as const; export const SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE = 8078012 as const; @@ -327,7 +327,7 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH | typeof SOLANA_ERROR__CODECS__EXPECTED_VARIABLE_LENGTH | typeof SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH - | typeof SOLANA_ERROR__CODECS__INVALID_DATA_ENUM_VARIANT + | typeof SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT | typeof SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS | typeof SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT | typeof SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 4ec10c02d894..2ea2897494be 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -18,7 +18,7 @@ import { SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, - SOLANA_ERROR__CODECS__INVALID_DATA_ENUM_VARIANT, + SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT, SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS, SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE, @@ -280,7 +280,7 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< codecDescription: string; expected: number; }; - [SOLANA_ERROR__CODECS__INVALID_DATA_ENUM_VARIANT]: { + [SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT]: { value: number | string | symbol; variants: (number | string | symbol)[]; }; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index c1a1ce4917d6..85a42d0a9c39 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -26,7 +26,7 @@ import { SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, SOLANA_ERROR__CODECS__EXPECTED_VARIABLE_LENGTH, SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, - SOLANA_ERROR__CODECS__INVALID_DATA_ENUM_VARIANT, + SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT, SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS, SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT, SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE, @@ -265,8 +265,8 @@ export const SolanaErrorMessages: Readonly<{ [SOLANA_ERROR__CODECS__EXPECTED_VARIABLE_LENGTH]: 'Expected a variable-size codec, got a fixed-size one.', [SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH]: 'Codec [$codecDescription] expected $expected bytes, got $bytesLength.', - [SOLANA_ERROR__CODECS__INVALID_DATA_ENUM_VARIANT]: - 'Invalid data enum variant. Expected one of [$variants], got $value.', + [SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT]: + 'Invalid discriminated union variant. Expected one of [$variants], got $value.', [SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS]: 'Expected [$codecDescription] to have $expected items, got $actual.', [SOLANA_ERROR__CODECS__INVALID_SCALAR_ENUM_VARIANT]: