Skip to content

Commit

Permalink
Improve the serialization logic for scalar enum
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva committed Apr 18, 2023
1 parent 957e769 commit 4973c97
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 56 deletions.
6 changes: 6 additions & 0 deletions .changeset/tough-pens-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@metaplex-foundation/umi-serializer-data-view': patch
'@metaplex-foundation/umi-serializer-beet': patch
---

Improve the serialization logic for scalar enum
49 changes: 25 additions & 24 deletions packages/umi-serializer-beet/src/createBeetSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,30 +388,30 @@ export function createBeetSerializer(
constructor: ScalarEnum<T> & {},
options: EnumSerializerOptions = {}
): Serializer<T> => {
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<T>, number] {
if (typeof value === 'number') {
return [enumValues[value], value];
}
const variantValue = constructor[value as keyof ScalarEnum<T>];
if (typeof variantValue === 'number') {
return [value as keyof ScalarEnum<T>, variantValue];
}
const indexOfValue = enumValues.indexOf(variantValue);
if (indexOfValue >= 0) {
return [variantValue as keyof ScalarEnum<T>, indexOfValue];
}
return [value as keyof ScalarEnum<T>, enumValues.indexOf(value)];
}
function checkVariantExists(variantKey: keyof ScalarEnum<T>): 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}`
);
}
}
Expand All @@ -420,19 +420,20 @@ 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) {
throw new DeserializingEmptyBufferError('enum');
}
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];
},
};
};
Expand Down
17 changes: 13 additions & 4 deletions packages/umi-serializer-beet/test/enum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down
49 changes: 25 additions & 24 deletions packages/umi-serializer-data-view/src/createDataViewSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,30 +388,30 @@ export function createDataViewSerializer(
constructor: ScalarEnum<T> & {},
options: EnumSerializerOptions = {}
): Serializer<T> => {
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<T>, number] {
if (typeof value === 'number') {
return [enumValues[value], value];
}
const variantValue = constructor[value as keyof ScalarEnum<T>];
if (typeof variantValue === 'number') {
return [value as keyof ScalarEnum<T>, variantValue];
}
const indexOfValue = enumValues.indexOf(variantValue);
if (indexOfValue >= 0) {
return [variantValue as keyof ScalarEnum<T>, indexOfValue];
}
return [value as keyof ScalarEnum<T>, enumValues.indexOf(value)];
}
function checkVariantExists(variantKey: keyof ScalarEnum<T>): 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}`
);
}
}
Expand All @@ -420,19 +420,20 @@ 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) {
throw new DeserializingEmptyBufferError('enum');
}
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];
},
};
};
Expand Down
17 changes: 13 additions & 4 deletions packages/umi-serializer-data-view/test/enum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down

0 comments on commit 4973c97

Please sign in to comment.