-
Notifications
You must be signed in to change notification settings - Fork 915
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(experimental): add getUnionCodec to @solana/codecs-data-stru…
…ctures (#2398) This PR adds a new `getUnionCodec` helper that can be used to encode/decode any TypeScript union. It accepts the following arguments: - An array of codecs, each defining a variant of the union. - A `getIndexFromValue` function which, given a value of the union, returns the index of the codec that should be used to encode that value. - A `getIndexFromBytes` function which, given the byte array to decode at a given offset, returns the index of the codec that should be used to decode the next bytes. ```ts const codec: Codec<number | boolean> = getUnionCodec( [getU16Codec(), getBooleanCodec()], value => (typeof value === 'number' ? 0 : 1), (bytes, offset) => (bytes.slice(offset).length > 1 ? 0 : 1), ); codec.encode(42); // 0x2a00 codec.encode(true); // 0x01 ```
- Loading branch information
1 parent
a548de2
commit bef9604
Showing
10 changed files
with
402 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
--- | ||
'@solana/codecs-data-structures': patch | ||
'@solana/errors': patch | ||
--- | ||
|
||
Added a new `getUnionCodec` helper that can be used to encode/decode any TypeScript union. | ||
|
||
```ts | ||
const codec: Codec<number | boolean> = getUnionCodec( | ||
[getU16Codec(), getBooleanCodec()], | ||
value => (typeof value === 'number' ? 0 : 1), | ||
(bytes, offset) => (bytes.slice(offset).length > 1 ? 0 : 1), | ||
); | ||
|
||
codec.encode(42); // 0x2a00 | ||
codec.encode(true); // 0x01 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
packages/codecs-data-structures/src/__tests__/union-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { assertIsFixedSize, assertIsVariableSize, fixCodec, mapCodec } from '@solana/codecs-core'; | ||
import { getU8Codec, getU16Codec } from '@solana/codecs-numbers'; | ||
import { getUtf8Codec } from '@solana/codecs-strings'; | ||
import { SOLANA_ERROR__CODECS__UNION_VARIANT_OUT_OF_RANGE, SolanaError } from '@solana/errors'; | ||
|
||
import { getBooleanCodec } from '../boolean'; | ||
import { getStructCodec } from '../struct'; | ||
import { getUnionCodec } from '../union'; | ||
import { b } from './__setup__'; | ||
|
||
describe('getUnionCodec', () => { | ||
const codec = getUnionCodec( | ||
[ | ||
fixCodec(getUtf8Codec(), 8), // 8 bytes. | ||
getU16Codec(), // 2 bytes. | ||
getBooleanCodec(), // 1 byte. | ||
getStructCodec([ | ||
// 4 bytes. | ||
['x', getU16Codec()], | ||
['y', getU16Codec()], | ||
]), | ||
], | ||
value => { | ||
if (value === 999) return 999; | ||
if (typeof value === 'string') return 0; | ||
if (typeof value === 'number') return 1; | ||
if (typeof value === 'boolean') return 2; | ||
return 3; | ||
}, | ||
bytes => { | ||
if (bytes.length === 3 && [...bytes].every(byte => byte === 255)) return 999; | ||
if (bytes.length === 8) return 0; | ||
if (bytes.length === 2) return 1; | ||
if (bytes.length === 1) return 2; | ||
return 3; | ||
}, | ||
); | ||
|
||
it('encodes any valid union variant', () => { | ||
expect(codec.encode('hello')).toStrictEqual(b('68656c6c6f000000')); | ||
expect(codec.encode(42)).toStrictEqual(b('2a00')); | ||
expect(codec.encode(true)).toStrictEqual(b('01')); | ||
expect(codec.encode({ x: 1, y: 2 })).toStrictEqual(b('01000200')); | ||
}); | ||
|
||
it('decodes any valid union variant', () => { | ||
expect(codec.decode(b('68656c6c6f000000'))).toBe('hello'); | ||
expect(codec.decode(b('2a00'))).toBe(42); | ||
expect(codec.decode(b('01'))).toBe(true); | ||
expect(codec.decode(b('01000200'))).toStrictEqual({ x: 1, y: 2 }); | ||
}); | ||
|
||
it('pushes the offset forward when writing', () => { | ||
expect(codec.write(42, new Uint8Array(10), 6)).toBe(8); | ||
}); | ||
|
||
it('pushes the offset forward when reading', () => { | ||
expect(codec.read(b('00'), 0)).toStrictEqual([false, 1]); | ||
}); | ||
|
||
it('throws when encoding an invalid variant', () => { | ||
expect(() => codec.encode(999)).toThrow( | ||
new SolanaError(SOLANA_ERROR__CODECS__UNION_VARIANT_OUT_OF_RANGE, { | ||
maxRange: 3, | ||
minRange: 0, | ||
variant: 999, | ||
}), | ||
); | ||
}); | ||
|
||
it('throws when decoding an invalid variant', () => { | ||
expect(() => codec.decode(b('ffffff'))).toThrow( | ||
new SolanaError(SOLANA_ERROR__CODECS__UNION_VARIANT_OUT_OF_RANGE, { | ||
maxRange: 3, | ||
minRange: 0, | ||
variant: 999, | ||
}), | ||
); | ||
}); | ||
|
||
it('returns a variable size codec', () => { | ||
assertIsVariableSize(codec); | ||
expect(codec.getSizeFromValue('hello')).toBe(8); | ||
expect(codec.getSizeFromValue(42)).toBe(2); | ||
expect(codec.getSizeFromValue(true)).toBe(1); | ||
expect(codec.getSizeFromValue({ x: 1, y: 2 })).toBe(4); | ||
expect(codec.maxSize).toBe(8); | ||
}); | ||
|
||
it('returns a fixed size codec when all variants have the same fixed size', () => { | ||
const sameSizeCodec = getUnionCodec( | ||
[getU8Codec(), getBooleanCodec()], | ||
() => 0, | ||
() => 0, | ||
); | ||
assertIsFixedSize(sameSizeCodec); | ||
expect(sameSizeCodec.fixedSize).toBe(1); | ||
}); | ||
|
||
it('can be used to create a zeroable nullable codec', () => { | ||
const nullCodec = mapCodec( | ||
getU8Codec(), | ||
(_value: null) => 0xff, | ||
() => null, | ||
); | ||
const zeroableCodec = getUnionCodec( | ||
[nullCodec, getU8Codec()], | ||
value => Number(value !== null), | ||
(bytes, offset) => Number(bytes[offset] !== 0xff), | ||
); | ||
expect(zeroableCodec.encode(null)).toStrictEqual(b('ff')); | ||
expect(zeroableCodec.encode(42)).toStrictEqual(b('2a')); | ||
expect(zeroableCodec.decode(b('ff'))).toBeNull(); | ||
expect(zeroableCodec.decode(b('2a'))).toBe(42); | ||
}); | ||
|
||
it('can be used to create a discriminated union codec', () => { | ||
const staticU16One = mapCodec( | ||
getU16Codec(), | ||
(_value: 1) => 1, | ||
() => 1 as const, | ||
); | ||
const staticU16Two = mapCodec( | ||
getU16Codec(), | ||
(_value: 2) => 2, | ||
() => 2 as const, | ||
); | ||
const discriminatedUnionCodec = getUnionCodec( | ||
[ | ||
getStructCodec([ | ||
['header', getU16Codec()], | ||
['type', staticU16One], | ||
]), | ||
getStructCodec([ | ||
['size', getU16Codec()], | ||
['type', staticU16Two], | ||
]), | ||
], | ||
value => value.type - 1, | ||
(bytes, offset) => bytes[offset + 2] - 1, | ||
); | ||
expect(discriminatedUnionCodec.encode({ header: 42, type: 1 })).toStrictEqual(b('2a000100')); | ||
expect(discriminatedUnionCodec.encode({ size: 9, type: 2 })).toStrictEqual(b('09000200')); | ||
expect(discriminatedUnionCodec.decode(b('2a000100'))).toStrictEqual({ header: 42, type: 1 }); | ||
expect(discriminatedUnionCodec.decode(b('09000200'))).toStrictEqual({ size: 9, type: 2 }); | ||
}); | ||
}); |
45 changes: 45 additions & 0 deletions
45
packages/codecs-data-structures/src/__typetests__/union-typetest.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { Codec, Decoder, Encoder } from '@solana/codecs-core'; | ||
|
||
import { getUnionCodec, getUnionDecoder, getUnionEncoder } from '../union'; | ||
|
||
const getIndex = () => 0; | ||
|
||
// [getUnionEncoder] It constructs unions from a list of encoder variants. | ||
{ | ||
getUnionEncoder( | ||
[ | ||
{} as Encoder<null>, | ||
{} as Encoder<bigint | number>, | ||
{} as Encoder<{ value: string }>, | ||
{} as Encoder<{ x: number; y: number }>, | ||
], | ||
getIndex, | ||
) satisfies Encoder<bigint | number | { value: string } | { x: number; y: number } | null>; | ||
} | ||
|
||
// [getUnionDecoder] It constructs unions from a list of decoder variants. | ||
{ | ||
getUnionDecoder( | ||
[ | ||
{} as Decoder<null>, | ||
{} as Decoder<bigint | number>, | ||
{} as Decoder<{ value: string }>, | ||
{} as Decoder<{ x: number; y: number }>, | ||
], | ||
getIndex, | ||
) satisfies Decoder<bigint | number | { value: string } | { x: number; y: number } | null>; | ||
} | ||
|
||
// [getUnionCodec] It constructs unions from a list of codec variants. | ||
{ | ||
getUnionCodec( | ||
[ | ||
{} as Codec<null>, | ||
{} as Codec<bigint | number>, | ||
{} as Codec<{ value: string }>, | ||
{} as Codec<{ x: number; y: number }>, | ||
], | ||
getIndex, | ||
getIndex, | ||
) satisfies Codec<bigint | number | { value: string } | { x: number; y: number } | null>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.