Skip to content

Commit

Permalink
refactor(experimental): rename getScalarEnumCodec to getEnumCodec (#2383
Browse files Browse the repository at this point in the history
)

Now that "Data Enum" is called "Discriminated Union", "Scalar Enum" can also be appropriately renamed as "Enum".

See #2296.
  • Loading branch information
lorisleiva committed Mar 27, 2024
1 parent 7e86583 commit ce1be3f
Show file tree
Hide file tree
Showing 14 changed files with 465 additions and 449 deletions.
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

0 comments on commit ce1be3f

Please sign in to comment.