diff --git a/.changeset/tough-pens-clean.md b/.changeset/tough-pens-clean.md new file mode 100644 index 00000000..2978f58d --- /dev/null +++ b/.changeset/tough-pens-clean.md @@ -0,0 +1,6 @@ +--- +'@metaplex-foundation/umi-serializer-data-view': patch +'@metaplex-foundation/umi-serializer-beet': patch +--- + +Improve the serialization logic for scalar enum diff --git a/packages/umi-serializer-beet/src/createBeetSerializer.ts b/packages/umi-serializer-beet/src/createBeetSerializer.ts index 0e0f1407..b2e3088d 100644 --- a/packages/umi-serializer-beet/src/createBeetSerializer.ts +++ b/packages/umi-serializer-beet/src/createBeetSerializer.ts @@ -388,30 +388,30 @@ export function createBeetSerializer( constructor: ScalarEnum & {}, options: EnumSerializerOptions = {} ): Serializer => { + const enumKeys = Object.keys(constructor); const enumValues = Object.values(constructor); const isNumericEnum = enumValues.some((v) => typeof v === 'number'); const valueDescriptions = enumValues .filter((v) => typeof v === 'string') .join(', '); - function getVariantKeyValue(value: T): [keyof ScalarEnum, number] { - if (typeof value === 'number') { - return [enumValues[value], value]; - } - const variantValue = constructor[value as keyof ScalarEnum]; - if (typeof variantValue === 'number') { - return [value as keyof ScalarEnum, variantValue]; - } - const indexOfValue = enumValues.indexOf(variantValue); - if (indexOfValue >= 0) { - return [variantValue as keyof ScalarEnum, indexOfValue]; - } - return [value as keyof ScalarEnum, enumValues.indexOf(value)]; - } - function checkVariantExists(variantKey: keyof ScalarEnum): void { - if (!enumValues.includes(variantKey)) { + const minRange = 0; + const maxRange = isNumericEnum + ? enumValues.length / 2 - 1 + : enumValues.length - 1; + const stringValues: string[] = isNumericEnum + ? [...enumKeys] + : [...new Set([...enumKeys, ...enumValues])]; + function assertValidVariant(variant: number | string): void { + const isInvalidNumber = + typeof variant === 'number' && + (variant < minRange || variant > maxRange); + const isInvalidString = + typeof variant === 'string' && !stringValues.includes(variant); + if (isInvalidNumber || isInvalidString) { throw new BeetSerializerError( - `Invalid enum variant. Got "${variantKey}", expected one of ` + - `[${enumValues.join(', ')}]` + `Invalid enum variant. Got "${variant}", ` + + `expected one of [${stringValues.join(', ')}] ` + + `or a number between ${minRange} and ${maxRange}` ); } } @@ -420,9 +420,11 @@ export function createBeetSerializer( fixedSize: 1, maxSize: 1, serialize: (value: T) => { - const [variantKey, variantValue] = getVariantKeyValue(value); - checkVariantExists(variantKey); - return u8().serialize(variantValue); + assertValidVariant(value as string | number); + if (typeof value === 'number') return u8().serialize(value); + const valueIndex = enumValues.indexOf(value); + if (valueIndex >= 0) return u8().serialize(valueIndex); + return u8().serialize(enumKeys.indexOf(value as string)); }, deserialize: (bytes: Uint8Array, offset = 0) => { if (bytes.slice(offset).length === 0) { @@ -430,9 +432,8 @@ export function createBeetSerializer( } const [value, newOffset] = u8().deserialize(bytes, offset); offset = newOffset; - const [variantKey, variantValue] = getVariantKeyValue(value as T); - checkVariantExists(variantKey); - return [(isNumericEnum ? variantValue : variantKey) as T, offset]; + assertValidVariant(value); + return [(isNumericEnum ? value : enumValues[value]) as T, offset]; }, }; }; diff --git a/packages/umi-serializer-beet/test/enum.test.ts b/packages/umi-serializer-beet/test/enum.test.ts index 262de2a1..cda14b00 100644 --- a/packages/umi-serializer-beet/test/enum.test.ts +++ b/packages/umi-serializer-beet/test/enum.test.ts @@ -32,13 +32,17 @@ test('numerical enum (de)serialization', (t) => { d(t, scalarEnum(Feedback), '01', Feedback.GOOD, 1); d(t, scalarEnum(Feedback), ['ffff01', 2], Feedback.GOOD, 3); - // Invalid example. + // Invalid examples. t.throws(() => scalarEnum(Feedback).serialize('Missing'), { message: (m: string) => m.includes( - 'Invalid enum variant. Got "Missing", expected one of [BAD, GOOD, 0, 1]' + 'Invalid enum variant. Got "Missing", expected one of [0, 1, BAD, GOOD] ' + + 'or a number between 0 and 1' ), }); + t.throws(() => scalarEnum(Feedback).deserialize(new Uint8Array([2])), { + message: /Invalid enum variant\. Got "2"/, + }); }); test('lexical enum (de)serialization', (t) => { @@ -72,13 +76,18 @@ test('lexical enum (de)serialization', (t) => { d(t, scalarEnum(Direction), '03', Direction.RIGHT, 1); d(t, scalarEnum(Direction), ['ffff03', 2], Direction.RIGHT, 3); - // Invalid example. + // Invalid examples. t.throws(() => scalarEnum(Direction).serialize('Diagonal' as any), { message: (m: string) => m.includes( - 'Invalid enum variant. Got "Diagonal", expected one of [Up, Down, Left, Right]' + 'Invalid enum variant. Got "Diagonal", expected one of ' + + '[UP, DOWN, LEFT, RIGHT, Up, Down, Left, Right] ' + + 'or a number between 0 and 3' ), }); + t.throws(() => scalarEnum(Direction).deserialize(new Uint8Array([4])), { + message: /Invalid enum variant\. Got "4"/, + }); }); test('description', (t) => { diff --git a/packages/umi-serializer-data-view/src/createDataViewSerializer.ts b/packages/umi-serializer-data-view/src/createDataViewSerializer.ts index df66f7e0..19179f1e 100644 --- a/packages/umi-serializer-data-view/src/createDataViewSerializer.ts +++ b/packages/umi-serializer-data-view/src/createDataViewSerializer.ts @@ -388,30 +388,30 @@ export function createDataViewSerializer( constructor: ScalarEnum & {}, options: EnumSerializerOptions = {} ): Serializer => { + const enumKeys = Object.keys(constructor); const enumValues = Object.values(constructor); const isNumericEnum = enumValues.some((v) => typeof v === 'number'); const valueDescriptions = enumValues .filter((v) => typeof v === 'string') .join(', '); - function getVariantKeyValue(value: T): [keyof ScalarEnum, number] { - if (typeof value === 'number') { - return [enumValues[value], value]; - } - const variantValue = constructor[value as keyof ScalarEnum]; - if (typeof variantValue === 'number') { - return [value as keyof ScalarEnum, variantValue]; - } - const indexOfValue = enumValues.indexOf(variantValue); - if (indexOfValue >= 0) { - return [variantValue as keyof ScalarEnum, indexOfValue]; - } - return [value as keyof ScalarEnum, enumValues.indexOf(value)]; - } - function checkVariantExists(variantKey: keyof ScalarEnum): void { - if (!enumValues.includes(variantKey)) { + const minRange = 0; + const maxRange = isNumericEnum + ? enumValues.length / 2 - 1 + : enumValues.length - 1; + const stringValues: string[] = isNumericEnum + ? [...enumKeys] + : [...new Set([...enumKeys, ...enumValues])]; + function assertValidVariant(variant: number | string): void { + const isInvalidNumber = + typeof variant === 'number' && + (variant < minRange || variant > maxRange); + const isInvalidString = + typeof variant === 'string' && !stringValues.includes(variant); + if (isInvalidNumber || isInvalidString) { throw new DataViewSerializerError( - `Invalid enum variant. Got "${variantKey}", expected one of ` + - `[${enumValues.join(', ')}]` + `Invalid enum variant. Got "${variant}", ` + + `expected one of [${stringValues.join(', ')}] ` + + `or a number between ${minRange} and ${maxRange}` ); } } @@ -420,9 +420,11 @@ export function createDataViewSerializer( fixedSize: 1, maxSize: 1, serialize: (value: T) => { - const [variantKey, variantValue] = getVariantKeyValue(value); - checkVariantExists(variantKey); - return u8().serialize(variantValue); + assertValidVariant(value as string | number); + if (typeof value === 'number') return u8().serialize(value); + const valueIndex = enumValues.indexOf(value); + if (valueIndex >= 0) return u8().serialize(valueIndex); + return u8().serialize(enumKeys.indexOf(value as string)); }, deserialize: (bytes: Uint8Array, offset = 0) => { if (bytes.slice(offset).length === 0) { @@ -430,9 +432,8 @@ export function createDataViewSerializer( } const [value, newOffset] = u8().deserialize(bytes, offset); offset = newOffset; - const [variantKey, variantValue] = getVariantKeyValue(value as T); - checkVariantExists(variantKey); - return [(isNumericEnum ? variantValue : variantKey) as T, offset]; + assertValidVariant(value); + return [(isNumericEnum ? value : enumValues[value]) as T, offset]; }, }; }; diff --git a/packages/umi-serializer-data-view/test/enum.test.ts b/packages/umi-serializer-data-view/test/enum.test.ts index d0372085..22a91576 100644 --- a/packages/umi-serializer-data-view/test/enum.test.ts +++ b/packages/umi-serializer-data-view/test/enum.test.ts @@ -33,13 +33,17 @@ test('numerical enum (de)serialization', (t) => { d(t, scalarEnum(Feedback), '01', Feedback.GOOD, 1); d(t, scalarEnum(Feedback), ['ffff01', 2], Feedback.GOOD, 3); - // Invalid example. + // Invalid examples. t.throws(() => scalarEnum(Feedback).serialize('Missing'), { message: (m: string) => m.includes( - 'Invalid enum variant. Got "Missing", expected one of [BAD, GOOD, 0, 1]' + 'Invalid enum variant. Got "Missing", expected one of [0, 1, BAD, GOOD] ' + + 'or a number between 0 and 1' ), }); + t.throws(() => scalarEnum(Feedback).deserialize(new Uint8Array([2])), { + message: /Invalid enum variant\. Got "2"/, + }); }); test('lexical enum (de)serialization', (t) => { @@ -73,13 +77,18 @@ test('lexical enum (de)serialization', (t) => { d(t, scalarEnum(Direction), '03', Direction.RIGHT, 1); d(t, scalarEnum(Direction), ['ffff03', 2], Direction.RIGHT, 3); - // Invalid example. + // Invalid examples. t.throws(() => scalarEnum(Direction).serialize('Diagonal' as any), { message: (m: string) => m.includes( - 'Invalid enum variant. Got "Diagonal", expected one of [Up, Down, Left, Right]' + 'Invalid enum variant. Got "Diagonal", expected one of ' + + '[UP, DOWN, LEFT, RIGHT, Up, Down, Left, Right] ' + + 'or a number between 0 and 3' ), }); + t.throws(() => scalarEnum(Direction).deserialize(new Uint8Array([4])), { + message: /Invalid enum variant\. Got "4"/, + }); }); test('description', (t) => {