Skip to content

Commit

Permalink
feat: support mixed native and non-native enums (#2501)
Browse files Browse the repository at this point in the history
* feat: support mixed native and non native enums

* chore: changeset

* chore: rearrange tests

* chore: use coder constant

Co-authored-by: Peter Smith <[email protected]>

---------

Co-authored-by: Peter Smith <[email protected]>
  • Loading branch information
danielbate and petertonysmith94 committed Jun 13, 2024
1 parent 4ba32d1 commit 436f040
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-shrimps-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/abi-coder": patch
---

feat: support mixed native and non-native enums
32 changes: 32 additions & 0 deletions packages/abi-coder/src/encoding/coders/EnumCoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { U64_MAX } from '../../../test/utils/constants';
import { BigNumberCoder } from './BigNumberCoder';
import { BooleanCoder } from './BooleanCoder';
import { EnumCoder } from './EnumCoder';
import { TupleCoder } from './TupleCoder';

/**
* @group node
Expand All @@ -32,6 +33,19 @@ describe('EnumCoder', () => {
expect(actual).toStrictEqual(expected);
});

it('encodes a native enum', () => {
const nativeCoder = new EnumCoder('Native', {
One: new BooleanCoder(),
Two: new BigNumberCoder('u64'),
Three: new TupleCoder([]),
});

const expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2]);
// @ts-expect-error native
const actual = nativeCoder.encode('Three');
expect(actual).toStrictEqual(expected);
});

it('should throw an error when encoding if no enum key is provided', async () => {
await expectToThrowFuelError(
() => coder.encode({} as never),
Expand Down Expand Up @@ -72,6 +86,24 @@ describe('EnumCoder', () => {
expect(actualLength).toBe(expectedLength);
});

it('decodes a native enum', () => {
const nativeCoder = new EnumCoder('TestEnum', {
a: new BooleanCoder(),
b: new BigNumberCoder('u64'),
c: new TupleCoder([]),
});

const expectedValue = 'c';
const expectedLength = 8;
const [actualValue, actualLength] = nativeCoder.decode(
new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2]),
0
);

expect(actualValue).toStrictEqual(expectedValue);
expect(actualLength).toBe(expectedLength);
});

it('should throw an error when decoded value accesses an invalid index', async () => {
const input = new Uint8Array(Array.from(Array(8).keys()));

Expand Down
19 changes: 12 additions & 7 deletions packages/abi-coder/src/encoding/coders/EnumCoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { hasNestedOption } from '../../utils/utilities';
import type { TypesOfCoder } from './AbstractCoder';
import { Coder } from './AbstractCoder';
import { BigNumberCoder } from './BigNumberCoder';
import type { TupleCoder } from './TupleCoder';

export type InputValueOf<TCoders extends Record<string, Coder>> = RequireExactlyOne<{
[P in keyof TCoders]: TypesOfCoder<TCoders[P]>['Input'];
Expand All @@ -17,12 +18,6 @@ export type DecodedValueOf<TCoders extends Record<string, Coder>> = RequireExact
[P in keyof TCoders]: TypesOfCoder<TCoders[P]>['Decoded'];
}>;

const isFullyNativeEnum = (enumCoders: { [s: string]: unknown } | ArrayLike<unknown>): boolean =>
Object.values(enumCoders).every(
// @ts-expect-error complicated types
({ type, coders }) => type === '()' && JSON.stringify(coders) === JSON.stringify([])
);

export class EnumCoder<TCoders extends Record<string, Coder>> extends Coder<
InputValueOf<TCoders>,
DecodedValueOf<TCoders>
Expand All @@ -47,6 +42,16 @@ export class EnumCoder<TCoders extends Record<string, Coder>> extends Coder<
this.#shouldValidateLength = !(this.type === OPTION_CODER_TYPE || hasNestedOption(coders));
}

// We parse a native enum as an empty tuple, so we are looking for a tuple with no child coders.
// The '()' is enough but the child coders is a stricter check.
#isNativeEnum(coder: Coder): boolean {
if (this.type !== OPTION_CODER_TYPE && coder.type === '()') {
const tupleCoder = coder as TupleCoder<[]>;
return tupleCoder.coders.length === 0;
}
return false;
}

#encodeNativeEnum(value: string): Uint8Array {
const valueCoder = this.coders[value];
const encodedValue = valueCoder.encode([]);
Expand Down Expand Up @@ -112,7 +117,7 @@ export class EnumCoder<TCoders extends Record<string, Coder>> extends Coder<

const [decoded, newOffset] = valueCoder.decode(data, offsetAndCase);

if (isFullyNativeEnum(this.coders)) {
if (this.#isNativeEnum(this.coders[caseKey])) {
return this.#decodeNativeEnum(caseKey, newOffset);
}

Expand Down
21 changes: 21 additions & 0 deletions packages/fuel-gauge/src/coverage-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ enum ColorEnumOutput {
Blue = 'Blue',
}

enum MixedNativeEnum {
Native = 'Native',
NotNative = 12,
}

/**
* @group node
*/
Expand Down Expand Up @@ -556,6 +561,22 @@ describe('Coverage Contract', () => {
expect(value).toStrictEqual(OUTPUT);
});

it('should test mixed native enum [Native->NotNative]', async () => {
const input = MixedNativeEnum.Native;
const expected = { NotNative: MixedNativeEnum.NotNative };

const { value } = await contractInstance.functions.mixed_native_enum(input).call();
expect(value).toStrictEqual(expected);
});

it('should test mixed native enum [NotNative->Native]', async () => {
const input = { NotNative: MixedNativeEnum.NotNative };
const expected = 'Native';

const { value } = await contractInstance.functions.mixed_native_enum(input).call();
expect(value).toStrictEqual(expected);
});

it('should try vec_as_only_param', async () => {
const { value } = await contractInstance.functions
.vec_as_only_param([100, 450, 202, 340])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ pub enum ColorEnum {
Blue: (),
}

pub enum MixedNativeEnum {
Native: (),
NotNative: u32,
}

enum MyContractError {
DivisionByZero: (),
}
Expand Down Expand Up @@ -109,6 +114,7 @@ abi CoverageContract {
fn echo_struct_vector_first(vector: Vec<BigStruct>) -> BigStruct;
fn echo_struct_vector_last(vector: Vec<ComplexStruct>) -> ComplexStruct;
fn color_enum(input: ColorEnum) -> ColorEnum;
fn mixed_native_enum(input: MixedNativeEnum) -> MixedNativeEnum;
fn vec_as_only_param(input: Vec<u64>) -> (u64, Option<u64>, Option<u64>, Option<u64>);
fn u32_and_vec_params(foo: u32, input: Vec<u64>) -> (u64, Option<u64>, Option<u64>, Option<u64>);
fn vec_in_vec(arg: Vec<Vec<u32>>);
Expand Down Expand Up @@ -408,6 +414,13 @@ impl CoverageContract for Contract {
}
}

fn mixed_native_enum(input: MixedNativeEnum) -> MixedNativeEnum {
match input {
MixedNativeEnum::Native => MixedNativeEnum::NotNative(12),
MixedNativeEnum::NotNative => MixedNativeEnum::Native,
}
}

fn vec_as_only_param(input: Vec<u64>) -> (u64, Option<u64>, Option<u64>, Option<u64>) {
(input.len(), input.get(0), input.get(1), input.get(2))
}
Expand Down

0 comments on commit 436f040

Please sign in to comment.