Skip to content

Commit

Permalink
refactor(experimental): add read write functions to option codecs
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva committed Dec 1, 2023
1 parent b2f08a6 commit 38fe2c9
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 125 deletions.
37 changes: 20 additions & 17 deletions packages/options/src/__tests__/__setup__.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import { Codec } from '@solana/codecs-core';
import { Codec, createCodec } from "@solana/codecs-core";

export const b = (s: string) => base16.encode(s);

export const base16: Codec<string> = {
decode(bytes, offset = 0) {
export const base16: Codec<string> = createCodec({
getSizeFromValue: (value: string) => Math.ceil(value.length / 2),
read(bytes, offset) {
const value = bytes.slice(offset).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
return [value, bytes.length];
},
description: 'base16',
encode(value: string) {
write(value: string, bytes, offset) {
const matches = value.toLowerCase().match(/.{1,2}/g);
return Uint8Array.from(matches ? matches.map((byte: string) => parseInt(byte, 16)) : []);
const hexBytes = matches ? matches.map((byte: string) => parseInt(byte, 16)) : [];
bytes.set(hexBytes, offset);
return offset + hexBytes.length;
},
fixedSize: null,
maxSize: null,
};
});

export const getMockCodec = (
config: {
defaultValue?: string;
description?: string;
size?: number | null;
} = {},
) => ({
decode: jest.fn().mockReturnValue([config.defaultValue ?? '', 0]),
description: config.description ?? 'mock',
encode: jest.fn().mockReturnValue(new Uint8Array()),
fixedSize: config.size ?? null,
maxSize: config.size ?? null,
});
} = {}
) =>
createCodec({
...(config.size != null ? { fixedSize: config.size } : { getSizeFromValue: jest.fn().mockReturnValue(0) }),
read: jest.fn().mockReturnValue([config.defaultValue ?? '', 0]),
write: jest.fn().mockReturnValue(0),
}) as Codec<unknown> & {
readonly read: jest.Mock;
readonly getSizeFromValue: jest.Mock;
readonly write: jest.Mock;
};
84 changes: 34 additions & 50 deletions packages/options/src/__tests__/option-codec-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getU8Codec, getU16Codec, getU64Codec } from '@solana/codecs-numbers';

import { none, some } from '../option';
import { getOptionCodec } from '../option-codec';
import { b, getMockCodec } from './__setup__';
import { b, base16, getMockCodec } from './__setup__';

describe('getOptionCodec', () => {
const option = getOptionCodec;
Expand All @@ -14,56 +14,60 @@ describe('getOptionCodec', () => {
// None.
expect(option(u8()).encode(none())).toStrictEqual(b('00'));
expect(option(u8()).encode(null)).toStrictEqual(b('00'));
expect(option(u8()).decode(b('00'))).toStrictEqual([none(), 1]);
expect(option(u8()).decode(b('ffff00'), 2)).toStrictEqual([none(), 3]);
expect(option(u8()).read(b('00'), 0)).toStrictEqual([none(), 1]);
expect(option(u8()).read(b('ffff00'), 2)).toStrictEqual([none(), 3]);

// None with custom prefix.
expect(option(u8(), { prefix: u16() }).encode(none())).toStrictEqual(b('0000'));
expect(option(u8(), { prefix: u16() }).encode(null)).toStrictEqual(b('0000'));
expect(option(u8(), { prefix: u16() }).decode(b('0000'))).toStrictEqual([none(), 2]);
expect(option(u8(), { prefix: u16() }).read(b('0000'), 0)).toStrictEqual([none(), 2]);

// Some.
expect(option(u8()).encode(some(42))).toStrictEqual(b('012a'));
expect(option(u8()).encode(42)).toStrictEqual(b('012a'));
expect(option(u8()).decode(b('012a'))).toStrictEqual([some(42), 2]);
expect(option(u8()).decode(b('ffff012a'), 2)).toStrictEqual([some(42), 4]);
expect(option(u8()).read(b('012a'), 0)).toStrictEqual([some(42), 2]);
expect(option(u8()).read(b('ffff012a'), 2)).toStrictEqual([some(42), 4]);

// Some with custom prefix.
expect(option(u8(), { prefix: u16() }).encode(some(42))).toStrictEqual(b('01002a'));
expect(option(u8(), { prefix: u16() }).encode(42)).toStrictEqual(b('01002a'));
expect(option(u8(), { prefix: u16() }).decode(b('01002a'))).toStrictEqual([some(42), 3]);
expect(option(u8(), { prefix: u16() }).read(b('01002a'), 0)).toStrictEqual([some(42), 3]);

// Some with variable-size codec.
const variableSizeMock = getMockCodec({ size: null });
variableSizeMock.encode.mockReturnValue(b('7777777777'));
variableSizeMock.decode.mockReturnValue(['Hello', 6]);
variableSizeMock.getSizeFromValue.mockReturnValue(5);
variableSizeMock.write.mockImplementation((_, bytes: Uint8Array, offset: number) => {
bytes.set(b('7777777777'), offset);
return offset + 5;
});
variableSizeMock.read.mockReturnValue(['Hello', 6]);
expect(option(variableSizeMock).encode(some('Hello'))).toStrictEqual(b('017777777777'));
expect(variableSizeMock.encode).toHaveBeenCalledWith('Hello');
expect(variableSizeMock.write).toHaveBeenCalledWith('Hello', expect.any(Uint8Array), 1);
expect(option(variableSizeMock).encode('Hello')).toStrictEqual(b('017777777777'));
expect(variableSizeMock.encode).toHaveBeenCalledWith('Hello');
expect(option(variableSizeMock).decode(b('017777777777'))).toStrictEqual([some('Hello'), 6]);
expect(variableSizeMock.decode).toHaveBeenCalledWith(b('017777777777'), 1);
expect(variableSizeMock.write).toHaveBeenCalledWith('Hello', expect.any(Uint8Array), 1);
expect(option(variableSizeMock).read(b('017777777777'), 0)).toStrictEqual([some('Hello'), 6]);
expect(variableSizeMock.read).toHaveBeenCalledWith(b('017777777777'), 1);

// Different From and To types.
const optionU64 = option<number | bigint, bigint>(u64());
expect(optionU64.encode(some(2))).toStrictEqual(b('010200000000000000'));
expect(optionU64.encode(some(2n))).toStrictEqual(b('010200000000000000'));
expect(optionU64.encode(2)).toStrictEqual(b('010200000000000000'));
expect(optionU64.encode(2n)).toStrictEqual(b('010200000000000000'));
expect(optionU64.decode(b('010200000000000000'))).toStrictEqual([some(2n), 9]);
expect(optionU64.read(b('010200000000000000'), 0)).toStrictEqual([some(2n), 9]);

// Nested options.
const nested = option(option(u8()));
expect(nested.encode(some(some(42)))).toStrictEqual(b('01012a'));
expect(nested.encode(some(42))).toStrictEqual(b('01012a'));
expect(nested.encode(42)).toStrictEqual(b('01012a'));
expect(nested.decode(b('01012a'))).toStrictEqual([some(some(42)), 3]);
expect(nested.read(b('01012a'), 0)).toStrictEqual([some(some(42)), 3]);
expect(nested.encode(some(none()))).toStrictEqual(b('0100'));
expect(nested.encode(some(null))).toStrictEqual(b('0100'));
expect(nested.decode(b('0100'))).toStrictEqual([some(none()), 2]);
expect(nested.read(b('0100'), 0)).toStrictEqual([some(none()), 2]);
expect(nested.encode(none())).toStrictEqual(b('00'));
expect(nested.encode(null)).toStrictEqual(b('00'));
expect(nested.decode(b('00'))).toStrictEqual([none(), 1]);
expect(nested.read(b('00'), 0)).toStrictEqual([none(), 1]);
});

it('encodes fixed options', () => {
Expand All @@ -73,70 +77,50 @@ describe('getOptionCodec', () => {
// None.
expect(fixedU8.encode(none())).toStrictEqual(b('0000'));
expect(fixedU8.encode(null)).toStrictEqual(b('0000'));
expect(fixedU8.decode(b('0000'))).toStrictEqual([none(), 2]);
expect(fixedU8.decode(b('ffff0000'), 2)).toStrictEqual([none(), 4]);
expect(fixedU8.read(b('0000'), 0)).toStrictEqual([none(), 2]);
expect(fixedU8.read(b('ffff0000'), 2)).toStrictEqual([none(), 4]);

// None with custom prefix.
expect(fixedU8WithU16Prefix.encode(none())).toStrictEqual(b('000000'));
expect(fixedU8WithU16Prefix.encode(null)).toStrictEqual(b('000000'));
expect(fixedU8WithU16Prefix.decode(b('000000'))).toStrictEqual([none(), 3]);
expect(fixedU8WithU16Prefix.read(b('000000'), 0)).toStrictEqual([none(), 3]);

// Some.
expect(fixedU8.encode(some(42))).toStrictEqual(b('012a'));
expect(fixedU8.encode(42)).toStrictEqual(b('012a'));
expect(fixedU8.decode(b('012a'))).toStrictEqual([some(42), 2]);
expect(fixedU8.decode(b('ffff012a'), 2)).toStrictEqual([some(42), 4]);
expect(fixedU8.read(b('012a'), 0)).toStrictEqual([some(42), 2]);
expect(fixedU8.read(b('ffff012a'), 2)).toStrictEqual([some(42), 4]);

// Some with custom prefix.
expect(fixedU8WithU16Prefix.encode(42)).toStrictEqual(b('01002a'));
expect(fixedU8WithU16Prefix.decode(b('01002a'))).toStrictEqual([some(42), 3]);
expect(fixedU8WithU16Prefix.read(b('01002a'), 0)).toStrictEqual([some(42), 3]);

// Different From and To types.
const optionU64 = option<number | bigint, bigint>(u64());
expect(optionU64.encode(some(2))).toStrictEqual(b('010200000000000000'));
expect(optionU64.encode(some(2n))).toStrictEqual(b('010200000000000000'));
expect(optionU64.encode(2)).toStrictEqual(b('010200000000000000'));
expect(optionU64.encode(2n)).toStrictEqual(b('010200000000000000'));
expect(optionU64.decode(b('010200000000000000'))).toStrictEqual([some(2n), 9]);
expect(optionU64.read(b('010200000000000000'), 0)).toStrictEqual([some(2n), 9]);

// Fixed options must wrap fixed-size items.
// @ts-expect-error Fixed options must wrap fixed-size items.
expect(() => option(getMockCodec({ size: null }), { fixed: true })).toThrow(
'Fixed options can only be used with fixed-size codecs',
);
});

it('has the right description', () => {
const mock = getMockCodec({ description: 'mock', size: 5 });
expect(option(u8()).description).toBe('option(u8; u8)');
expect(option(mock).description).toBe('option(mock; u8)');
expect(option(u8(), { prefix: u16() }).description).toBe('option(u8; u16(le))');

// Fixed.
expect(option(u8(), { fixed: true }).description).toBe('option(u8; u8; fixed)');
expect(option(mock, { fixed: true }).description).toBe('option(mock; u8; fixed)');
expect(option(u8(), { fixed: true, prefix: u16() }).description).toBe('option(u8; u16(le); fixed)');

// Custom description.
expect(option(u8(), { description: 'My option' }).description).toBe('My option');
});

it('has the right sizes', () => {
const fixMock = getMockCodec({ description: 'mock', size: 5 });
const variableMock = getMockCodec({ description: 'mock', size: null });

expect(option(u8()).fixedSize).toBeNull();
expect(option(u8()).getSizeFromValue(some(42))).toBe(1 + 1);
expect(option(u8()).maxSize).toBe(2);
expect(option(variableMock).fixedSize).toBeNull();
expect(option(variableMock).maxSize).toBeNull();
expect(option(u8(), { prefix: u16() }).fixedSize).toBeNull();
expect(option(base16).getSizeFromValue(some('010203'))).toBe(1 + 3);
expect(option(base16).maxSize).toBeUndefined();
expect(option(u8(), { prefix: u16() }).getSizeFromValue(some(42))).toBe(2 + 1);
expect(option(u8(), { prefix: u16() }).maxSize).toBe(3);

// Fixed.
expect(option(u8(), { fixed: true }).fixedSize).toBe(2);
expect(option(u8(), { fixed: true }).maxSize).toBe(2);
expect(option(fixMock, { fixed: true }).fixedSize).toBe(6);
expect(option(fixMock, { fixed: true }).maxSize).toBe(6);
expect(option(u64(), { fixed: true }).fixedSize).toBe(9);
expect(option(u8(), { fixed: true, prefix: u16() }).fixedSize).toBe(3);
expect(option(u8(), { fixed: true, prefix: u16() }).maxSize).toBe(3);
});
});
46 changes: 46 additions & 0 deletions packages/options/src/__typetests__/option-codec-typetest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
FixedSizeCodec,
FixedSizeDecoder,
FixedSizeEncoder,
VariableSizeCodec,
VariableSizeDecoder,
VariableSizeEncoder,
} from '@solana/codecs-core';

import { Option, OptionOrNullable } from '../option';
import { getOptionCodec, getOptionDecoder, getOptionEncoder } from '../option-codec';

{
// [getOptionEncoder]: It knows if the encoder is fixed size or variable size.
getOptionEncoder({} as FixedSizeEncoder<void, 0>) satisfies FixedSizeEncoder<OptionOrNullable<void>>;
getOptionEncoder({} as FixedSizeEncoder<string>, { fixed: true }) satisfies FixedSizeEncoder<
OptionOrNullable<string>
>;
getOptionEncoder({} as FixedSizeEncoder<string>) satisfies VariableSizeEncoder<OptionOrNullable<string>>;

// @ts-expect-error It cannot be fixed when using a variable size item.
getOptionEncoder({} as VariableSizeEncoder<string>, { fixed: true });
}

{
// [getOptionDecoder]: It knows if the decoder is fixed size or variable size.
getOptionDecoder({} as FixedSizeDecoder<void, 0>) satisfies FixedSizeDecoder<Option<void>>;
getOptionDecoder({} as FixedSizeDecoder<string>, { fixed: true }) satisfies FixedSizeDecoder<Option<string>>;
getOptionDecoder({} as FixedSizeDecoder<string>) satisfies VariableSizeDecoder<Option<string>>;

// @ts-expect-error It cannot be fixed when using a variable size item.
getOptionDecoder({} as VariableSizeDecoder<string>, { fixed: true });
}

{
// [getOptionCodec]: It knows if the codec is fixed size or variable size.
getOptionCodec({} as FixedSizeCodec<void, void, 0>) satisfies FixedSizeCodec<OptionOrNullable<void>, Option<void>>;
getOptionCodec({} as FixedSizeCodec<string>, { fixed: true }) satisfies FixedSizeCodec<
OptionOrNullable<string>,
Option<string>
>;
getOptionCodec({} as FixedSizeCodec<string>) satisfies VariableSizeCodec<OptionOrNullable<string>, Option<string>>;

// @ts-expect-error It cannot be fixed when using a variable size item.
getOptionCodec({} as VariableSizeCodec<string>, { fixed: true });
}
Loading

0 comments on commit 38fe2c9

Please sign in to comment.