BTreeSet and BTreeMap - encoding and serialization fixes (#3789)

* BTreeSet and BTreeMap - encoding and serialization fixes

* Fix linter issues

* BTreeSet/BTreeMap: Support for Struct and Enum sorting

* Cleanup sorting function
This commit is contained in:
Lezek123
2021-07-26 07:41:25 +02:00
committed by GitHub
parent 9e7037af04
commit fcaebbdbb2
6 changed files with 293 additions and 9 deletions
+130 -2
View File
@@ -2,15 +2,52 @@
// SPDX-License-Identifier: Apache-2.0
import { TypeRegistry } from '../create';
import { Text, U32 } from '../primitive';
import { BTreeMap, Struct } from '.';
import { I32, Text, U32 } from '../primitive';
import { Constructor } from '../types';
import { ITuple } from '../types/interfaces';
import { BTreeMap, Enum, Struct, Tuple } from '.';
const registry = new TypeRegistry();
class U32TextTuple extends (Tuple.with([U32, Text]) as unknown as Constructor<ITuple<[U32, Text]>>) {}
// Reason: We purposefully want `text` to be the first key of the struct and take priority during sorting
// eslint-disable-next-line sort-keys
class MockStruct extends Struct.with({ text: Text, int: I32 }) {}
class MockEnum extends Enum.with({
Key1: MockStruct,
Key2: MockStruct,
Key3: U32TextTuple
}) {}
const mockU32TextMap = new Map<Text, U32>();
const mockU32TupleMap = new Map<ITuple<[U32, Text]>, U32>();
const mockU32I32Map = new Map<I32, U32>();
const mockU32StructMap = new Map<MockStruct, U32>();
const mockU32EnumMap = new Map<MockEnum, U32>();
mockU32TextMap.set(new Text(registry, 'bazzing'), new U32(registry, 69));
mockU32TupleMap.set((new U32TextTuple(registry, [2, 'ba'])), new U32(registry, 42));
mockU32TupleMap.set((new U32TextTuple(registry, [2, 'b'])), new U32(registry, 7));
mockU32TupleMap.set((new U32TextTuple(registry, [1, 'baz'])), new U32(registry, 13));
mockU32I32Map.set(new I32(registry, 255), new U32(registry, 69));
mockU32I32Map.set(new I32(registry, -255), new U32(registry, 42));
mockU32I32Map.set(new I32(registry, 1000), new U32(registry, 7));
mockU32I32Map.set(new I32(registry, -1000), new U32(registry, 25));
mockU32I32Map.set(new I32(registry, 0), new U32(registry, 13));
mockU32StructMap.set(new MockStruct(registry, { int: 1, text: 'b' }), new U32(registry, 42));
mockU32StructMap.set(new MockStruct(registry, { int: -1, text: 'b' }), new U32(registry, 7));
mockU32StructMap.set(new MockStruct(registry, { int: -1, text: 'ba' }), new U32(registry, 25));
mockU32StructMap.set(new MockStruct(registry, { int: -2, text: 'baz' }), new U32(registry, 13));
mockU32EnumMap.set(new MockEnum(registry, { Key3: new U32TextTuple(registry, [2, 'ba']) }), new U32(registry, 13));
mockU32EnumMap.set(new MockEnum(registry, { Key3: new U32TextTuple(registry, [2, 'b']) }), new U32(registry, 42));
mockU32EnumMap.set(new MockEnum(registry, { Key2: new MockStruct(registry, { int: -1, text: 'b' }) }), new U32(registry, 7));
mockU32EnumMap.set(new MockEnum(registry, { Key1: new MockStruct(registry, { int: 1, text: 'b' }) }), new U32(registry, 25));
mockU32EnumMap.set(new MockEnum(registry, { Key1: new MockStruct(registry, { int: -1, text: 'b' }) }), new U32(registry, 69));
describe('BTreeMap', (): void => {
it('decodes null', (): void => {
expect(
@@ -54,6 +91,97 @@ describe('BTreeMap', (): void => {
).toEqual(13);
});
it('correctly sorts simple keys', (): void => {
expect(
Array.from(new (BTreeMap.with(I32, U32))(registry, mockU32I32Map).keys()).map((k) => k.toNumber())
).toEqual([-1000, -255, 0, 255, 1000]);
});
it('correctly sorts tuple keys', (): void => {
expect(
Array.from(new (BTreeMap.with(U32TextTuple, U32))(registry, mockU32TupleMap).keys()).map((k) => k.toJSON())
).toEqual([[1, 'baz'], [2, 'b'], [2, 'ba']]);
});
it('correctly sorts struct keys', (): void => {
expect(
Array.from(new (BTreeMap.with(MockStruct, U32))(registry, mockU32StructMap).keys()).map((k) => k.toJSON())
).toEqual([
{ int: -1, text: 'b' },
{ int: 1, text: 'b' },
{ int: -1, text: 'ba' },
{ int: -2, text: 'baz' }
]);
});
it('correctly sorts enum keys', (): void => {
expect(
Array.from(new (BTreeMap.with(MockEnum, U32))(registry, mockU32EnumMap).keys()).map((k) => k.toJSON())
).toEqual([
{ key1: { int: -1, text: 'b' } },
{ key1: { int: 1, text: 'b' } },
{ key2: { int: -1, text: 'b' } },
{ key3: [2, 'b'] },
{ key3: [2, 'ba'] }
]);
});
it('correctly serializes/deserializes to/from json with numeric keys', (): void => {
expect(
new (BTreeMap.with(I32, U32))(
registry,
new (BTreeMap.with(I32, U32))(registry, mockU32I32Map).toJSON()
).toJSON()
).toEqual({ '-1000': 25, '-255': 42, 0: 13, 1000: 7, 255: 69 });
});
it('correctly serializes/deserializes to/from json with text keys', (): void => {
expect(
new (BTreeMap.with(Text, U32))(
registry,
new (BTreeMap.with(Text, U32))(registry, mockU32TextMap).toJSON()
).toJSON()
).toEqual({ bazzing: 69 });
});
it('correctly serializes/deserializes to/from json with tuple keys', (): void => {
expect(
new (BTreeMap.with(U32TextTuple, U32))(
registry,
new (BTreeMap.with(U32TextTuple, U32))(registry, mockU32TupleMap).toJSON()
).toJSON()
).toEqual({ '[1,"baz"]': 13, '[2,"b"]': 7, '[2,"ba"]': 42 });
});
it('correctly serializes/deserializes to/from json with struct keys', (): void => {
expect(
new (BTreeMap.with(MockStruct, U32))(
registry,
new (BTreeMap.with(MockStruct, U32))(registry, mockU32StructMap).toJSON()
).toJSON()
).toEqual({
'{"text":"b","int":-1}': 7,
'{"text":"b","int":1}': 42,
'{"text":"ba","int":-1}': 25,
'{"text":"baz","int":-2}': 13
});
});
it('correctly serializes/deserializes to/from json with enum keys', (): void => {
expect(
new (BTreeMap.with(MockEnum, U32))(
registry,
new (BTreeMap.with(MockEnum, U32))(registry, mockU32EnumMap).toJSON()
).toJSON()
).toEqual({
'{"key1":{"text":"b","int":-1}}': 69,
'{"key1":{"text":"b","int":1}}': 25,
'{"key2":{"text":"b","int":-1}}': 7,
'{"key3":[2,"b"]}': 42,
'{"key3":[2,"ba"]}': 13
});
});
it('generates sane toRawTypes', (): void => {
expect(new (BTreeMap.with(Text, U32))(registry).toRawType()).toBe('BTreeMap<Text,u32>');
expect(new (BTreeMap.with(Text, Text))(registry).toRawType()).toBe('BTreeMap<Text,Text>');
+84 -2
View File
@@ -4,11 +4,24 @@
import type { CodecTo } from '../types';
import { TypeRegistry } from '../create';
import { Text, U32 } from '../primitive';
import { BTreeSet, Struct } from '.';
import { I32, Text, U32 } from '../primitive';
import { Constructor } from '../types';
import { ITuple } from '../types/interfaces';
import { Enum } from './Enum';
import { BTreeSet, Struct, Tuple } from '.';
const registry = new TypeRegistry();
class U32TextTuple extends (Tuple.with([U32, Text]) as unknown as Constructor<ITuple<[U32, Text]>>) {}
// Reason: We purposefully want `text` to be the first key of the struct and take priority during sorting
// eslint-disable-next-line sort-keys
class MockStruct extends Struct.with({ text: Text, int: I32 }) {}
class MockEnum extends Enum.with({
Key1: MockStruct,
Key2: MockStruct,
Key3: U32TextTuple
}) {}
const mockU32Set = new Set<U32>();
mockU32Set.add(new U32(registry, 2));
@@ -21,6 +34,34 @@ const mockU32SetObject = [2, 24, 30, 80];
const mockU32SetHexString = '0x1002000000180000001e00000050000000';
const mockU32SetUint8Array = Uint8Array.from([16, 2, 0, 0, 0, 24, 0, 0, 0, 30, 0, 0, 0, 80, 0, 0, 0]);
const mockI32SetObj = [1000, 0, 255, -255, -1000];
const mockTextSetObj = [
new Text(registry, 'baz'),
new Text(registry, 'b'),
new Text(registry, 'bb'),
new Text(registry, 'ba'),
new Text(registry, 'c')
];
const mockTupleSetObj = [
new U32TextTuple(registry, [2, 'ba']),
new U32TextTuple(registry, [2, 'bb']),
new U32TextTuple(registry, [2, 'b']),
new U32TextTuple(registry, [1, 'baz'])
];
const mockStructSetObj = [
new MockStruct(registry, { int: 1, text: 'b' }),
new MockStruct(registry, { int: -1, text: 'b' }),
new MockStruct(registry, { int: -1, text: 'ba' }),
new MockStruct(registry, { int: -2, text: 'baz' })
];
const mockEnumSetObj = [
new MockEnum(registry, { Key3: new U32TextTuple(registry, [2, 'ba']) }),
new MockEnum(registry, { Key3: new U32TextTuple(registry, [2, 'b']) }),
new MockEnum(registry, { Key2: new MockStruct(registry, { int: -1, text: 'b' }) }),
new MockEnum(registry, { Key1: new MockStruct(registry, { int: 1, text: 'b' }) }),
new MockEnum(registry, { Key1: new MockStruct(registry, { int: -1, text: 'b' }) })
];
describe('BTreeSet', (): void => {
describe('decoding', (): void => {
const testDecode = (type: string, input: any, output: string): void =>
@@ -94,6 +135,47 @@ describe('BTreeSet', (): void => {
).toEqual(17);
});
it('correctly sorts numeric values', (): void => {
expect(
Array.from(new (BTreeSet.with(I32))(registry, mockI32SetObj)).map((k) => k.toNumber())
).toEqual([-1000, -255, 0, 255, 1000]);
});
it('correctly sorts text values', (): void => {
expect(
Array.from(new (BTreeSet.with(Text))(registry, mockTextSetObj)).map((k) => k.toString())
).toEqual(['b', 'ba', 'baz', 'bb', 'c']);
});
it('correctly sorts complex tuple values', (): void => {
expect(
Array.from(new (BTreeSet.with(U32TextTuple))(registry, mockTupleSetObj)).map((k) => k.toJSON())
).toEqual([[1, 'baz'], [2, 'b'], [2, 'ba'], [2, 'bb']]);
});
it('correctly sorts complex struct values', (): void => {
expect(
Array.from(new (BTreeSet.with(MockStruct))(registry, mockStructSetObj)).map((k) => k.toJSON())
).toEqual([
{ int: -1, text: 'b' },
{ int: 1, text: 'b' },
{ int: -1, text: 'ba' },
{ int: -2, text: 'baz' }
]);
});
it('correctly sorts complex enum values', (): void => {
expect(
Array.from(new (BTreeSet.with(MockEnum))(registry, mockEnumSetObj)).map((k) => k.toJSON())
).toEqual([
{ key1: { int: -1, text: 'b' } },
{ key1: { int: 1, text: 'b' } },
{ key2: { int: -1, text: 'b' } },
{ key3: [2, 'b'] },
{ key3: [2, 'ba'] }
]);
});
it('generates sane toRawTypes', (): void => {
expect(new (BTreeSet.with(U32))(registry).toRawType()).toBe('BTreeSet<u32>');
expect(new (BTreeSet.with(Text))(registry).toRawType()).toBe('BTreeSet<Text>');
+2 -2
View File
@@ -6,7 +6,7 @@ import type { AnyJson, Codec, Constructor, InterfaceTypes, Registry } from '../t
import { compactFromU8a, compactToU8a, isHex, isU8a, logger, stringify, u8aConcat, u8aToHex, u8aToU8a } from '@polkadot/util';
import { compareSet, decodeU8a, typeToConstructor } from './utils';
import { compareSet, decodeU8a, sortSet, typeToConstructor } from './utils';
const l = logger('BTreeSet');
@@ -84,7 +84,7 @@ export class BTreeSet<V extends Codec = Codec> extends Set<V> implements Codec {
readonly #ValClass: Constructor<V>;
constructor (registry: Registry, valType: Constructor<V> | keyof InterfaceTypes, rawValue?: Uint8Array | string | string[] | Set<any>) {
super(decodeSet(registry, valType, rawValue));
super(sortSet(decodeSet(registry, valType, rawValue)));
this.registry = registry;
this.#ValClass = typeToConstructor(registry, valType);
+12 -3
View File
@@ -6,7 +6,10 @@ import type { AnyJson, Codec, Constructor, InterfaceTypes, Registry } from '../t
import { compactFromU8a, compactToU8a, isHex, isObject, isU8a, logger, stringify, u8aConcat, u8aToHex, u8aToU8a } from '@polkadot/util';
import { compareMap, decodeU8a, typeToConstructor } from './utils';
import { AbstractArray } from './AbstractArray';
import { Enum } from './Enum';
import { Struct } from './Struct';
import { compareMap, decodeU8a, sortMap, typeToConstructor } from './utils';
const l = logger('Map');
@@ -34,11 +37,15 @@ function decodeMapFromMap<K extends Codec = Codec, V extends Codec = Codec> (reg
const output = new Map<K, V>();
value.forEach((val: any, key: any) => {
const isComplex = KeyClass.prototype instanceof AbstractArray ||
KeyClass.prototype instanceof Struct ||
KeyClass.prototype instanceof Enum;
try {
output.set(
key instanceof KeyClass
? key
: new KeyClass(registry, key),
: new KeyClass(registry, isComplex ? JSON.parse(key) : key),
val instanceof ValClass
? val
: new ValClass(registry, val)
@@ -97,7 +104,9 @@ export class CodecMap<K extends Codec = Codec, V extends Codec = Codec> extends
readonly #type: string;
constructor (registry: Registry, keyType: Constructor<K> | keyof InterfaceTypes, valType: Constructor<V> | keyof InterfaceTypes, rawValue: Uint8Array | string | Map<any, any> | undefined, type: 'BTreeMap' | 'HashMap' = 'HashMap') {
super(decodeMap(registry, keyType, valType, rawValue));
const decoded = decodeMap(registry, keyType, valType, rawValue);
super(type === 'BTreeMap' ? sortMap(decoded) : decoded);
this.registry = registry;
this.#KeyClass = typeToConstructor(registry, keyType);
+1
View File
@@ -7,3 +7,4 @@ export { compareSet } from './compareSet';
export { decodeU8a } from './decodeU8a';
export { mapToTypeMap } from './mapToTypeMap';
export { typeToConstructor } from './typeToConstructor';
export { sortAsc, sortSet, sortMap } from './sortValues';
@@ -0,0 +1,64 @@
// Copyright 2017-2021 @polkadot/types authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Codec } from '../../types';
import { AbstractArray } from '../AbstractArray';
import { AbstractInt } from '../AbstractInt';
import { Enum, Struct } from '..';
type SortArg = Codec | Codec[] | number[] | number | Uint8Array
/** @internal **/
function isArrayLike (arg: SortArg): arg is AbstractArray<Codec> | Uint8Array | Codec[] | number[] {
return arg instanceof AbstractArray || arg instanceof Uint8Array || Array.isArray(arg);
}
/** @internal **/
function isCodec (arg: SortArg): arg is Codec {
return !!(arg && typeof (arg as Codec).toU8a === 'function');
}
/**
* Sort keys/values of BTreeSet/BTreeMap in acending order for encoding compatibility with Rust's BTreeSet/BTreeMap
* (https://doc.rust-lang.org/stable/std/collections/struct.BTreeSet.html)
* (https://doc.rust-lang.org/stable/std/collections/struct.BTreeMap.html)
*/
export function sortAsc<V extends SortArg = Codec> (a: V, b: V): number {
if (typeof a === 'number' && typeof b === 'number') {
return a - b;
} else if (a instanceof Struct && b instanceof Struct) {
return sortAsc(Array.from(a.values()), Array.from(b.values()));
} else if (a instanceof Enum && b instanceof Enum) {
return sortAsc(a.index, b.index) || sortAsc(a.value, b.value);
} else if (isArrayLike(a) && isArrayLike(b)) {
// Vec, Tuple, Bytes etc.
let sortRes = 0; const lenA = a.length; const lenB = b.length;
const minLen = Math.min(lenA, lenB);
for (let i = 0; i < minLen; ++i) {
sortRes = sortAsc(a[i], b[i]);
if (sortRes !== 0) {
return sortRes;
}
}
return lenA - lenB;
} else if (a instanceof AbstractInt && b instanceof AbstractInt) {
return a.cmp(b);
} else if (isCodec(a) && isCodec(b)) {
// Text, Bool etc.
return sortAsc(a.toU8a(true), b.toU8a(true));
} else {
throw new Error(`Attempting to sort unrecognized value: ${JSON.stringify(a)}`);
}
}
export function sortSet<V extends Codec = Codec> (set: Set<V>): Set<V> {
return new Set(Array.from(set).sort(sortAsc));
}
export function sortMap<K extends Codec = Codec, V extends Codec = Codec> (map: Map<K, V>): Map<K, V> {
return new Map(Array.from(map.entries()).sort(([keyA], [keyB]) => sortAsc(keyA, keyB)));
}