Skip to content

Commit

Permalink
refactor(experimental): rename fixCodec and prefixCodec to fixCodecSi…
Browse files Browse the repository at this point in the history
…ze and prefixCodecSize (#2411)

This PR renames `fixCodec` to `fixCodecSize` and `prefixCodec` to `prefixCodecSize` [as discussed here](#2397 (comment)).

Note that I created a new changeset describing the change for `fixCodec` but not for `prefixCodec` as this is not yet merged and I thought it'd be better to update the changeset created by a previous PR of this stack.
  • Loading branch information
lorisleiva committed Apr 2, 2024
1 parent 4ae78f5 commit 2e5af9f
Show file tree
Hide file tree
Showing 24 changed files with 204 additions and 201 deletions.
4 changes: 2 additions & 2 deletions .changeset/chatty-flies-end.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
'@solana/codecs-core': patch
---

Added a new `prefixCodec` primitive
Added a new `prefixCodecSize` primitive

```ts
const codec = prefixCodec(getBase58Codec(), getU32Codec());
const codec = prefixCodecSize(getBase58Codec(), getU32Codec());

codec.encode('hello world');
// 0x0b00000068656c6c6f20776f726c64
Expand Down
11 changes: 11 additions & 0 deletions .changeset/shiny-birds-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@solana/codecs-data-structures': patch
'@solana/codecs-strings': patch
'@solana/codecs-core': patch
'@solana/addresses': patch
'@solana/rpc-types': patch
'@solana/options': patch
'@solana/rpc-api': patch
---

Renamed `fixCodec` to `fixCodecSize`
2 changes: 1 addition & 1 deletion packages/addresses/src/__tests__/address-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe('Address', () => {
expect(() => address.decode(tooShortBuffer)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, {
bytesLength: 31,
codecDescription: 'fixCodec',
codecDescription: 'fixCodecSize',
expected: 32,
}),
);
Expand Down
28 changes: 14 additions & 14 deletions packages/codecs-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ There is a significant library of composable codecs at your disposal, enabling y
- [`@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.
You may also be interested in some of the helpers of this `@solana/codecs-core` library such as `mapCodec`, `fixCodecSize` or `reverseCodec` that create new codecs from existing ones.

Note that all of these libraries are included in the [`@solana/codecs` package](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs) as well as the main `@solana/web3.js` package for your convenience.

Expand Down Expand Up @@ -346,44 +346,44 @@ const getStringU32Codec = () => combineCodec(getStringU32Encoder(), getStringU32

## Fixing the size of codecs

The `fixCodec` function allows you to bind the size of a given codec to the given fixed size.
The `fixCodecSize` function allows you to bind the size of a given codec to the given fixed size.

For instance, say you want to represent a base-58 string that uses exactly 32 bytes when decoded. Here’s how you can use the `fixCodec` helper to achieve that.
For instance, say you want to represent a base-58 string that uses exactly 32 bytes when decoded. Here’s how you can use the `fixCodecSize` helper to achieve that.

```ts
const get32BytesBase58Codec = () => fixCodec(getBase58Codec(), 32);
const get32BytesBase58Codec = () => fixCodecSize(getBase58Codec(), 32);
```

You may also use the `fixEncoder` and `fixDecoder` functions to separate your codec logic like so:
You may also use the `fixEncoderSize` and `fixDecoderSize` functions to separate your codec logic like so:

```ts
const get32BytesBase58Encoder = () => fixEncoder(getBase58Encoder(), 32);
const get32BytesBase58Decoder = () => fixDecoder(getBase58Decoder(), 32);
const get32BytesBase58Codec = () => combineCodec(get32BytesBase58Encoder(), get32BytesBase58Codec());
const get32BytesBase58Encoder = () => fixEncoderSize(getBase58Encoder(), 32);
const get32BytesBase58Decoder = () => fixDecoderSize(getBase58Decoder(), 32);
const get32BytesBase58Codec = () => combineCodec(get32BytesBase58Encoder(), get32BytesBase58Decoder());
```

## Prefixing the size of codecs

The `prefixCodec` function allows you to store the byte size of any codec as a number prefix. This allows you to contain variable-size codecs to their actual size.
The `prefixCodecSize` function allows you to store the byte size of any codec as a number prefix. This allows you to contain variable-size codecs to their actual size.

When encoding, the size of the encoded data is stored before the encoded data itself. When decoding, the size is read first to know how many bytes to read next.

For example, say we want to represent a variable-size base-58 string using a `u32` size prefix — the equivalent of a Borsh `String` in Rust. Here’s how you can use the `prefixCodec` function to achieve that.
For example, say we want to represent a variable-size base-58 string using a `u32` size prefix — the equivalent of a Borsh `String` in Rust. Here’s how you can use the `prefixCodecSize` function to achieve that.

```ts
const getU32Base58Codec = () => prefixCodec(getBase58Codec(), getU32Codec());
const getU32Base58Codec = () => prefixCodecSize(getBase58Codec(), getU32Codec());

getU32Base58Codec().encode('hello world');
// 0x0b00000068656c6c6f20776f726c64
// | └-- Our encoded base-58 string.
// └-- Our encoded u32 size prefix.
```

You may also use the `prefixEncoder` and `prefixDecoder` functions to separate your codec logic like so:
You may also use the `prefixEncoderSize` and `prefixDecoderSize` functions to separate your codec logic like so:

```ts
const getU32Base58Encoder = () => prefixEncoder(getBase58Encoder(), getU32Encoder());
const getU32Base58Decoder = () => prefixDecoder(getBase58Decoder(), getU32Decoder());
const getU32Base58Encoder = () => prefixEncoderSize(getBase58Encoder(), getU32Encoder());
const getU32Base58Decoder = () => prefixDecoderSize(getBase58Decoder(), getU32Decoder());
const getU32Base58Codec = () => combineCodec(getU32Base58Encoder(), getU32Base58Decoder());
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, SolanaError } from '@solana/errors';

import { createCodec } from '../codec';
import { fixCodec, fixDecoder, fixEncoder } from '../fix-codec';
import { fixCodecSize, fixDecoderSize, fixEncoderSize } from '../fix-codec-size';
import { b, getMockCodec } from './__setup__';

describe('fixCodec', () => {
describe('fixCodecSize', () => {
it('keeps same-sized byte arrays as-is', () => {
const mockCodec = getMockCodec();

Expand All @@ -13,13 +13,13 @@ describe('fixCodec', () => {
bytes.set(b('08050c0c0f170f120c04'), offset);
return offset + 10;
});
expect(fixCodec(mockCodec, 10).encode('helloworld')).toStrictEqual(b('08050c0c0f170f120c04'));
expect(fixCodecSize(mockCodec, 10).encode('helloworld')).toStrictEqual(b('08050c0c0f170f120c04'));
expect(mockCodec.write).toHaveBeenCalledWith('helloworld', expect.any(Uint8Array), 0);

fixCodec(mockCodec, 10).decode(b('08050c0c0f170f120c04'));
fixCodecSize(mockCodec, 10).decode(b('08050c0c0f170f120c04'));
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f170f120c04'), 0);

fixCodec(mockCodec, 10).read(b('ffff08050c0c0f170f120c04'), 2);
fixCodecSize(mockCodec, 10).read(b('ffff08050c0c0f170f120c04'), 2);
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f170f120c04'), 0);
});

Expand All @@ -31,13 +31,13 @@ describe('fixCodec', () => {
bytes.set(b('08050c0c0f170f120c04'), offset);
return offset + 10;
});
expect(fixCodec(mockCodec, 5).encode('helloworld')).toStrictEqual(b('08050c0c0f'));
expect(fixCodecSize(mockCodec, 5).encode('helloworld')).toStrictEqual(b('08050c0c0f'));
expect(mockCodec.write).toHaveBeenCalledWith('helloworld', expect.any(Uint8Array), 0);

fixCodec(mockCodec, 5).decode(b('08050c0c0f170f120c04'));
fixCodecSize(mockCodec, 5).decode(b('08050c0c0f170f120c04'));
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f'), 0);

fixCodec(mockCodec, 5).read(b('ffff08050c0c0f170f120c04'), 2);
fixCodecSize(mockCodec, 5).read(b('ffff08050c0c0f170f120c04'), 2);
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f'), 0);
});

Expand All @@ -49,28 +49,28 @@ describe('fixCodec', () => {
bytes.set(b('08050c0c0f'), offset);
return offset + 5;
});
expect(fixCodec(mockCodec, 10).encode('hello')).toStrictEqual(b('08050c0c0f0000000000'));
expect(fixCodecSize(mockCodec, 10).encode('hello')).toStrictEqual(b('08050c0c0f0000000000'));
expect(mockCodec.write).toHaveBeenCalledWith('hello', expect.any(Uint8Array), 0);

fixCodec(mockCodec, 10).decode(b('08050c0c0f0000000000'));
fixCodecSize(mockCodec, 10).decode(b('08050c0c0f0000000000'));
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0);

fixCodec(mockCodec, 10).read(b('ffff08050c0c0f0000000000'), 2);
fixCodecSize(mockCodec, 10).read(b('ffff08050c0c0f0000000000'), 2);
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0);

expect(() => fixCodec(mockCodec, 10).decode(b('08050c0c0f'))).toThrow(
expect(() => fixCodecSize(mockCodec, 10).decode(b('08050c0c0f'))).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, {
bytesLength: 5,
codecDescription: 'fixCodec',
codecDescription: 'fixCodecSize',
expected: 10,
}),
);
});

it('has the right sizes', () => {
const mockCodec = getMockCodec({ size: null });
expect(fixCodec(mockCodec, 12).fixedSize).toBe(12);
expect(fixCodec(mockCodec, 42).fixedSize).toBe(42);
expect(fixCodecSize(mockCodec, 12).fixedSize).toBe(12);
expect(fixCodecSize(mockCodec, 42).fixedSize).toBe(42);
});

it('can fix a codec that requires a minimum amount of bytes', () => {
Expand All @@ -90,8 +90,8 @@ describe('fixCodec', () => {
},
});

// When we synthesize a `u24` from that `u32` using `fixCodec`.
const u24 = fixCodec(u32, 3);
// When we synthesize a `u24` from that `u32` using `fixCodecSize`.
const u24 = fixCodecSize(u32, 3);

// Then we can encode a `u24`.
const bytes = u24.encode(42);
Expand All @@ -103,7 +103,7 @@ describe('fixCodec', () => {
});
});

describe('fixEncoder', () => {
describe('fixEncoderSize', () => {
it('can fix an encoder to a given amount of bytes', () => {
const mockCodec = getMockCodec();

Expand All @@ -112,44 +112,44 @@ describe('fixEncoder', () => {
bytes.set(b('08050c0c0f170f120c04'), offset);
return offset + 10;
});
expect(fixEncoder(mockCodec, 10).encode('helloworld')).toStrictEqual(b('08050c0c0f170f120c04'));
expect(fixEncoderSize(mockCodec, 10).encode('helloworld')).toStrictEqual(b('08050c0c0f170f120c04'));
expect(mockCodec.write).toHaveBeenCalledWith('helloworld', expect.any(Uint8Array), 0);

mockCodec.getSizeFromValue.mockReturnValueOnce(10);
mockCodec.write.mockImplementationOnce((_, bytes: Uint8Array, offset: number) => {
bytes.set(b('08050c0c0f170f120c04'), offset);
return offset + 10;
});
expect(fixEncoder(mockCodec, 5).encode('helloworld')).toStrictEqual(b('08050c0c0f'));
expect(fixEncoderSize(mockCodec, 5).encode('helloworld')).toStrictEqual(b('08050c0c0f'));
expect(mockCodec.write).toHaveBeenCalledWith('helloworld', expect.any(Uint8Array), 0);

mockCodec.getSizeFromValue.mockReturnValueOnce(5);
mockCodec.write.mockImplementationOnce((_, bytes: Uint8Array, offset: number) => {
bytes.set(b('08050c0c0f'), offset);
return offset + 5;
});
expect(fixEncoder(mockCodec, 10).encode('hello')).toStrictEqual(b('08050c0c0f0000000000'));
expect(fixEncoderSize(mockCodec, 10).encode('hello')).toStrictEqual(b('08050c0c0f0000000000'));
expect(mockCodec.write).toHaveBeenCalledWith('hello', expect.any(Uint8Array), 0);
});
});

describe('fixDecoder', () => {
describe('fixDecoderSize', () => {
it('can fix a decoder to a given amount of bytes', () => {
const mockCodec = getMockCodec();

fixDecoder(mockCodec, 10).decode(b('08050c0c0f170f120c04'));
fixDecoderSize(mockCodec, 10).decode(b('08050c0c0f170f120c04'));
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f170f120c04'), 0);

fixDecoder(mockCodec, 5).decode(b('08050c0c0f170f120c04'));
fixDecoderSize(mockCodec, 5).decode(b('08050c0c0f170f120c04'));
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f'), 0);

fixDecoder(mockCodec, 10).decode(b('08050c0c0f0000000000'));
fixDecoderSize(mockCodec, 10).decode(b('08050c0c0f0000000000'));
expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0);

expect(() => fixDecoder(mockCodec, 10).decode(b('08050c0c0f'))).toThrow(
expect(() => fixDecoderSize(mockCodec, 10).decode(b('08050c0c0f'))).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, {
bytesLength: 5,
codecDescription: 'fixCodec',
codecDescription: 'fixCodecSize',
expected: 10,
}),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { assertIsFixedSize, assertIsVariableSize, Codec } from '../codec';
import { prefixCodec } from '../prefix-codec';
import { prefixCodecSize } from '../prefix-codec-size';
import { b, getMockCodec } from './__setup__';

describe('prefixCodec', () => {
describe('prefixCodecSize', () => {
it('encodes the byte length before the content', () => {
const numberCodec = getMockCodec({ size: 4 });
const contentCodec = getMockCodec({ size: 10 });
const prefixedCodec = prefixCodec(contentCodec, numberCodec as Codec<number>);
const prefixedCodec = prefixCodecSize(contentCodec, numberCodec as Codec<number>);

prefixedCodec.encode('helloworld');
expect(numberCodec.write).toHaveBeenCalledWith(10, expect.any(Uint8Array), 0);
Expand All @@ -17,7 +17,7 @@ describe('prefixCodec', () => {
const numberCodec = getMockCodec({ size: 4 });
numberCodec.read.mockReturnValue([10, 4]);
const contentCodec = getMockCodec({ size: 10 });
const prefixedCodec = prefixCodec(contentCodec, numberCodec as Codec<number>);
const prefixedCodec = prefixCodecSize(contentCodec, numberCodec as Codec<number>);

prefixedCodec.decode(b('0a00000068656c6c6f776f726c64'));
expect(numberCodec.read).toHaveBeenCalledWith(b('0a00000068656c6c6f776f726c64'), 0);
Expand All @@ -32,14 +32,14 @@ describe('prefixCodec', () => {
if (value > 255) throw overflowError;
});
const contentCodec = getMockCodec({ size: 256 });
const prefixedCodec = prefixCodec(contentCodec, numberCodec as Codec<number>);
const prefixedCodec = prefixCodecSize(contentCodec, numberCodec as Codec<number>);
expect(() => prefixedCodec.encode(null)).toThrow(overflowError);
});

it('returns the correct fixed size', () => {
const numberCodec = getMockCodec({ size: 4 });
const contentCodec = getMockCodec({ size: 10 });
const prefixedCodec = prefixCodec(contentCodec, numberCodec as Codec<number>);
const prefixedCodec = prefixCodecSize(contentCodec, numberCodec as Codec<number>);
assertIsFixedSize(prefixedCodec);
expect(prefixedCodec.fixedSize).toBe(14);
});
Expand All @@ -48,7 +48,7 @@ describe('prefixCodec', () => {
const numberCodec = getMockCodec({ size: 4 });
const contentCodec = getMockCodec();
contentCodec.getSizeFromValue.mockReturnValueOnce(10);
const prefixedCodec = prefixCodec(contentCodec, numberCodec as Codec<number>);
const prefixedCodec = prefixCodecSize(contentCodec, numberCodec as Codec<number>);
assertIsVariableSize(prefixedCodec);
expect(prefixedCodec.getSizeFromValue('helloworld')).toBe(14);
});
Expand Down
15 changes: 4 additions & 11 deletions packages/codecs-core/src/__tests__/reverse-codec-test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { SOLANA_ERROR__CODECS__EXPECTED_FIXED_LENGTH, SolanaError } from '@solana/errors';

import { createDecoder, createEncoder } from '../codec';
import { fixCodec } from '../fix-codec';
import { ReadonlyUint8Array } from '../readonly-uint8array';
import { fixCodecSize } from '../fix-codec-size';
import { reverseCodec, reverseDecoder, reverseEncoder } from '../reverse-codec';
import { b, base16 } from './__setup__';

describe('reverseCodec', () => {
it('can reverse the bytes of a fixed-size codec', () => {
const s = (size: number) => reverseCodec(fixCodec(base16, size));
const s = (size: number) => reverseCodec(fixCodecSize(base16, size));

// Encode.
expect(s(1).encode('00')).toStrictEqual(b('00'));
Expand Down Expand Up @@ -58,10 +57,7 @@ describe('reverseDecoder', () => {
it('can reverse the bytes of a fixed-size decoder', () => {
const decoder = createDecoder({
fixedSize: 2,
read: (bytes: ReadonlyUint8Array | Uint8Array, offset = 0) => [
`${bytes[offset]}-${bytes[offset + 1]}`,
offset + 2,
],
read: (bytes, offset = 0) => [`${bytes[offset]}-${bytes[offset + 1]}`, offset + 2],
});

const reversedDecoder = reverseDecoder(decoder);
Expand All @@ -75,10 +71,7 @@ describe('reverseDecoder', () => {
it('does not modify the input bytes in-place', () => {
const decoder = createDecoder({
fixedSize: 2,
read: (bytes: ReadonlyUint8Array | Uint8Array, offset = 0) => [
`${bytes[offset]}-${bytes[offset + 1]}`,
offset + 2,
],
read: (bytes, offset = 0) => [`${bytes[offset]}-${bytes[offset + 1]}`, offset + 2],
});

const reversedDecoder = reverseDecoder(decoder);
Expand Down
33 changes: 33 additions & 0 deletions packages/codecs-core/src/__typetests__/fix-codec-size-typetest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Codec,
Decoder,
Encoder,
FixedSizeCodec,
FixedSizeDecoder,
FixedSizeEncoder,
VariableSizeCodec,
VariableSizeDecoder,
VariableSizeEncoder,
} from '../codec';
import { fixCodecSize, fixDecoderSize, fixEncoderSize } from '../fix-codec-size';

{
// [fixEncoderSize]: It transforms any encoder into a fixed size encoder.
fixEncoderSize({} as FixedSizeEncoder<string>, 42) satisfies FixedSizeEncoder<string, 42>;
fixEncoderSize({} as VariableSizeEncoder<string>, 42) satisfies FixedSizeEncoder<string, 42>;
fixEncoderSize({} as Encoder<string>, 42) satisfies FixedSizeEncoder<string, 42>;
}

{
// [fixDecoderSize]: It transforms any decoder into a fixed size decoder.
fixDecoderSize({} as FixedSizeDecoder<string>, 42) satisfies FixedSizeDecoder<string, 42>;
fixDecoderSize({} as VariableSizeDecoder<string>, 42) satisfies FixedSizeDecoder<string, 42>;
fixDecoderSize({} as Decoder<string>, 42) satisfies FixedSizeDecoder<string, 42>;
}

{
// [fixCodecSize]: It transforms any codec into a fixed size codec.
fixCodecSize({} as FixedSizeCodec<string>, 42) satisfies FixedSizeCodec<string, string, 42>;
fixCodecSize({} as VariableSizeCodec<string>, 42) satisfies FixedSizeCodec<string, string, 42>;
fixCodecSize({} as Codec<string>, 42) satisfies FixedSizeCodec<string, string, 42>;
}

0 comments on commit 2e5af9f

Please sign in to comment.