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): rename getScalarEnumCodec to getEnumCodec #2383

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
8 changes: 8 additions & 0 deletions .changeset/loud-otters-pull.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
---

`getScalarEnumCodec` is now called `getEnumCodec`
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, discriminated unions, 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, 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
38 changes: 19 additions & 19 deletions packages/codecs-data-structures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ const bytes = personEncoder.encode({ name: 'alice', age: 42 });
const person = personDecoder.decode(bytes);
```

## Scalar enum codec
## Enum codec

The `getScalarEnumCodec` function accepts a JavaScript enum constructor and returns a codec for encoding and decoding values of that enum.
The `getEnumCodec` function accepts a JavaScript enum constructor and returns a codec for encoding and decoding values of that enum.

```ts
enum Direction {
Expand All @@ -191,11 +191,11 @@ enum Direction {
Down,
}

const bytes = getScalarEnumCodec(Direction).encode(Direction.Left);
const direction = getScalarEnumCodec(Direction).decode(bytes);
const bytes = getEnumCodec(Direction).encode(Direction.Left);
const direction = getEnumCodec(Direction).decode(bytes);
```

When encoding a scalar enum, you may pass the value as an enum value, as a number or even as a string by passing the variant’s name.
When encoding an enum, you may pass the value as an enum value, as a number or even as a string by passing the variant’s name.

```ts
enum Direction {
Expand All @@ -205,23 +205,23 @@ enum Direction {
Down,
}

getScalarEnumCodec(Direction).encode(Direction.Left); // 0x00
getScalarEnumCodec(Direction).encode(Direction.Right); // 0x01
getScalarEnumCodec(Direction).encode(0); // 0x00
getScalarEnumCodec(Direction).encode(1); // 0x01
getScalarEnumCodec(Direction).encode('Left'); // 0x00
getScalarEnumCodec(Direction).encode('Right'); // 0x01
getEnumCodec(Direction).encode(Direction.Left); // 0x00
getEnumCodec(Direction).encode(Direction.Right); // 0x01
getEnumCodec(Direction).encode(0); // 0x00
getEnumCodec(Direction).encode(1); // 0x01
getEnumCodec(Direction).encode('Left'); // 0x00
getEnumCodec(Direction).encode('Right'); // 0x01
```

As you can see, by default, a `u8` number is being used to store the enum value. However, a number codec may be passed as the `size` option to configure that behaviour.

```ts
const u32DirectionCodec = getScalarEnumCodec(Direction, { size: getU32Codec() });
const u32DirectionCodec = getEnumCodec(Direction, { size: getU32Codec() });
u32DirectionCodec.encode(Direction.Left); // 0x00000000
u32DirectionCodec.encode(Direction.Right); // 0x01000000
```

Note that if you provide a string enum — e.g. `enum Direction { Left = 'LEFT' }` — to the `getScalarEnumCodec` function, it will only store the index of the variant. However, the string value may be used to encode that index.
Note that if you provide a string enum — e.g. `enum Direction { Left = 'LEFT' }` — to the `getEnumCodec` function, it will only store the index of the variant. However, the string value may be used to encode that index.

```ts
enum Direction {
Expand All @@ -231,16 +231,16 @@ enum Direction {
Down = 'DOWN',
}

getScalarEnumCodec(Direction).encode(Direction.Right); // 0x01
getScalarEnumCodec(Direction).encode('Right' as Direction); // 0x01
getScalarEnumCodec(Direction).encode('RIGHT'); // 0x01
getEnumCodec(Direction).encode(Direction.Right); // 0x01
getEnumCodec(Direction).encode('Right' as Direction); // 0x01
getEnumCodec(Direction).encode('RIGHT'); // 0x01
```

Separate `getScalarEnumEncoder` and `getScalarEnumDecoder` functions are also available.
Separate `getEnumEncoder` and `getEnumDecoder` functions are also available.

```ts
const bytes = getScalarEnumEncoder(Direction).encode(Direction.Left);
const direction = getScalarEnumDecoder(Direction).decode(bytes);
const bytes = getEnumEncoder(Direction).encode(Direction.Left);
const direction = getEnumDecoder(Direction).decode(bytes);
```

## Discriminated union codec
Expand Down
159 changes: 159 additions & 0 deletions packages/codecs-data-structures/src/__tests__/enum-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { getU32Codec, getU64Codec } from '@solana/codecs-numbers';
import {
SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE,
SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT,
SolanaError,
} from '@solana/errors';

import { getEnumCodec } from '../enum';
import { b } from './__setup__';

describe('getEnumCodec', () => {
const u32 = getU32Codec;
const u64 = getU64Codec;

enum Empty {}
enum Feedback {
BAD,
GOOD,
}
enum Direction {
UP = 'Up',
DOWN = 'Down',
LEFT = 'Left',
RIGHT = 'Right',
}
enum Hybrid {
NUMERIC,
LEXICAL = 'Lexical',
}

it('encodes enums', () => {
// Bad.
expect(getEnumCodec(Feedback).encode(Feedback.BAD)).toStrictEqual(b('00'));
expect(getEnumCodec(Feedback).encode('BAD')).toStrictEqual(b('00'));
expect(getEnumCodec(Feedback).encode(0)).toStrictEqual(b('00'));
expect(getEnumCodec(Feedback).read(b('00'), 0)).toStrictEqual([Feedback.BAD, 1]);
expect(getEnumCodec(Feedback).read(b('ffff00'), 2)).toStrictEqual([Feedback.BAD, 3]);

// Good.
expect(getEnumCodec(Feedback).encode(Feedback.GOOD)).toStrictEqual(b('01'));
expect(getEnumCodec(Feedback).encode('GOOD')).toStrictEqual(b('01'));
expect(getEnumCodec(Feedback).encode(1)).toStrictEqual(b('01'));
expect(getEnumCodec(Feedback).read(b('01'), 0)).toStrictEqual([Feedback.GOOD, 1]);
expect(getEnumCodec(Feedback).read(b('ffff01'), 2)).toStrictEqual([Feedback.GOOD, 3]);

// Custom size.
const u64Feedback = getEnumCodec(Feedback, { size: u64() });
expect(u64Feedback.encode(Feedback.GOOD)).toStrictEqual(b('0100000000000000'));
expect(u64Feedback.read(b('0100000000000000'), 0)).toStrictEqual([Feedback.GOOD, 8]);

// Invalid examples.
// @ts-expect-error Invalid enum variant.
expect(() => getEnumCodec(Feedback).encode('Missing')).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, {
maxRange: 1,
minRange: 0,
value: 'Missing',
variants: ['BAD', 'GOOD'],
}),
);
expect(() => getEnumCodec(Feedback).read(new Uint8Array([2]), 0)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, {
discriminator: 2,
maxRange: 1,
minRange: 0,
}),
);
});

it('encodes lexical enums', () => {
// Up.
expect(getEnumCodec(Direction).encode(Direction.UP)).toStrictEqual(b('00'));
expect(getEnumCodec(Direction).encode('UP')).toStrictEqual(b('00'));
expect(getEnumCodec(Direction).encode('Up' as Direction)).toStrictEqual(b('00'));
expect(getEnumCodec(Direction).read(b('00'), 0)).toStrictEqual([Direction.UP, 1]);
expect(getEnumCodec(Direction).read(b('ffff00'), 2)).toStrictEqual([Direction.UP, 3]);

// Down.
expect(getEnumCodec(Direction).encode(Direction.DOWN)).toStrictEqual(b('01'));
expect(getEnumCodec(Direction).encode('DOWN')).toStrictEqual(b('01'));
expect(getEnumCodec(Direction).encode('Down' as Direction)).toStrictEqual(b('01'));
expect(getEnumCodec(Direction).read(b('01'), 0)).toStrictEqual([Direction.DOWN, 1]);
expect(getEnumCodec(Direction).read(b('ffff01'), 2)).toStrictEqual([Direction.DOWN, 3]);

// Left.
expect(getEnumCodec(Direction).encode(Direction.LEFT)).toStrictEqual(b('02'));
expect(getEnumCodec(Direction).encode('LEFT')).toStrictEqual(b('02'));
expect(getEnumCodec(Direction).encode('Left' as Direction)).toStrictEqual(b('02'));
expect(getEnumCodec(Direction).read(b('02'), 0)).toStrictEqual([Direction.LEFT, 1]);
expect(getEnumCodec(Direction).read(b('ffff02'), 2)).toStrictEqual([Direction.LEFT, 3]);

// Right.
expect(getEnumCodec(Direction).encode(Direction.RIGHT)).toStrictEqual(b('03'));
expect(getEnumCodec(Direction).encode('RIGHT')).toStrictEqual(b('03'));
expect(getEnumCodec(Direction).encode('Right' as Direction)).toStrictEqual(b('03'));
expect(getEnumCodec(Direction).read(b('03'), 0)).toStrictEqual([Direction.RIGHT, 1]);
expect(getEnumCodec(Direction).read(b('ffff03'), 2)).toStrictEqual([Direction.RIGHT, 3]);

// Invalid examples.
// @ts-expect-error Invalid enum variant.
expect(() => getEnumCodec(Direction).encode('Diagonal')).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, {
maxRange: 3,
minRange: 0,
value: 'Diagonal',
variants: ['UP', 'DOWN', 'LEFT', 'RIGHT', 'Up', 'Down', 'Left', 'Right'],
}),
);
expect(() => getEnumCodec(Direction).read(new Uint8Array([4]), 0)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, {
discriminator: 4,
maxRange: 3,
minRange: 0,
}),
);
});

it('encodes hybrid enums', () => {
// Numeric.
expect(getEnumCodec(Hybrid).encode(Hybrid.NUMERIC)).toStrictEqual(b('00'));
expect(getEnumCodec(Hybrid).encode('NUMERIC')).toStrictEqual(b('00'));
expect(getEnumCodec(Hybrid).encode(0)).toStrictEqual(b('00'));
expect(getEnumCodec(Hybrid).read(b('00'), 0)).toStrictEqual([Hybrid.NUMERIC, 1]);
expect(getEnumCodec(Hybrid).read(b('ffff00'), 2)).toStrictEqual([Hybrid.NUMERIC, 3]);

// Lexical.
expect(getEnumCodec(Hybrid).encode(Hybrid.LEXICAL)).toStrictEqual(b('01'));
expect(getEnumCodec(Hybrid).encode('LEXICAL')).toStrictEqual(b('01'));
expect(getEnumCodec(Hybrid).encode('Lexical' as Hybrid)).toStrictEqual(b('01'));
expect(getEnumCodec(Hybrid).read(b('01'), 0)).toStrictEqual([Hybrid.LEXICAL, 1]);
expect(getEnumCodec(Hybrid).read(b('ffff01'), 2)).toStrictEqual([Hybrid.LEXICAL, 3]);

// Invalid examples.
// @ts-expect-error Invalid enum variant.
expect(() => getEnumCodec(Hybrid).encode('Missing')).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, {
maxRange: 1,
minRange: 0,
value: 'Missing',
variants: ['NUMERIC', 'LEXICAL', 'Lexical'],
}),
);
expect(() => getEnumCodec(Hybrid).read(new Uint8Array([2]), 0)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__ENUM_DISCRIMINATOR_OUT_OF_RANGE, {
discriminator: 2,
maxRange: 1,
minRange: 0,
}),
);
});

it('has the right sizes', () => {
expect(getEnumCodec(Empty).fixedSize).toBe(1);
expect(getEnumCodec(Feedback).fixedSize).toBe(1);
expect(getEnumCodec(Direction).fixedSize).toBe(1);
expect(getEnumCodec(Hybrid).fixedSize).toBe(1);
expect(getEnumCodec(Feedback, { size: u32() }).fixedSize).toBe(4);
});
});
Loading
Loading