Basic BTreeMaps codec support (#1474)
* BTreeMap codec type * used proper, relative path * Register class types by default * reduced cognitive complexity * Update packages/types/src/codec/BTreeMap.ts Co-Authored-By: Jaco Greeff <jacogr@gmail.com> * Update packages/types/src/codec/BTreeMap.ts Co-Authored-By: Jaco Greeff <jacogr@gmail.com> * Update packages/types/src/codec/BTreeMap.spec.ts Co-Authored-By: Jaco Greeff <jacogr@gmail.com> * added copyright; logic fixes * added BTreeMap in alphabetical order * better unit tests multiple value examples taken from scale codec repo * lint fix
This commit is contained in:
committed by
Jaco Greeff
co-authored by
Jaco Greeff
parent
d5d8b59f8b
commit
acfd856b25
@@ -0,0 +1,112 @@
|
||||
// Copyright 2017-2019 @polkadot/types authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import '../injector';
|
||||
import Text from '../primitive/Text';
|
||||
import U32 from '../primitive/U32';
|
||||
import Struct from './Struct';
|
||||
import { CodecTo } from '../types';
|
||||
|
||||
import BTreeMap from './BTreeMap';
|
||||
|
||||
const mockU32TextMap = new Map<Text, U32>();
|
||||
mockU32TextMap.set(new Text('bazzing'), new U32(69));
|
||||
const mockU32TextMapString = '{"bazzing":69}';
|
||||
const mockU32TextMapObject = { bazzing: 69 };
|
||||
const mockU32TextMapHexString = '0x041c62617a7a696e6745000000';
|
||||
const mockU32TextMapUint8Array = Uint8Array.from([4, 28, 98, 97, 122, 122, 105, 110, 103, 69, 0, 0, 0]);
|
||||
|
||||
const mockU32U32Map = new Map<U32, U32>();
|
||||
mockU32U32Map.set(new U32(1), new U32(2));
|
||||
mockU32U32Map.set(new U32(23), new U32(24));
|
||||
mockU32U32Map.set(new U32(28), new U32(30));
|
||||
mockU32U32Map.set(new U32(45), new U32(80));
|
||||
const mockU32U32MapString = '{"1":2,"23":24,"28":30,"45":80}';
|
||||
const mockU32U32MapObject = { 1: 2, 23: 24, 28: 30, 45: 80 };
|
||||
const mockU32U32MapHexString = '0x10043102000000083233180000000832381e00000008343550000000';
|
||||
const mockU32U32MapUint8Array = Uint8Array.from([16, 4, 49, 2, 0, 0, 0, 8, 50, 51, 24, 0, 0, 0, 8, 50, 56, 30, 0, 0, 0, 8, 52, 53, 80, 0, 0, 0]);
|
||||
|
||||
describe('BTreeMap', (): void => {
|
||||
describe('decoding', (): void => {
|
||||
const testDecode = (type: string, input: any, output: string): void =>
|
||||
it(`can decode from ${type}`, (): void => {
|
||||
const s = new BTreeMap(Text, U32, input);
|
||||
|
||||
expect(s.toString()).toBe(output);
|
||||
});
|
||||
|
||||
testDecode('map', mockU32TextMap, mockU32TextMapString);
|
||||
testDecode('hex', mockU32TextMapHexString, mockU32TextMapString);
|
||||
testDecode('Uint8Array', mockU32TextMapUint8Array, mockU32TextMapString);
|
||||
|
||||
testDecode('map', mockU32U32Map, mockU32U32MapString);
|
||||
testDecode('hex', mockU32U32MapHexString, mockU32U32MapString);
|
||||
testDecode('Uint8Array', mockU32U32MapUint8Array, mockU32U32MapString);
|
||||
});
|
||||
|
||||
describe('encoding', (): void => {
|
||||
const testEncode = (to: CodecTo, expected: any): void =>
|
||||
it(`can encode ${to}`, (): void => {
|
||||
const s = new BTreeMap(Text, U32, mockU32TextMap);
|
||||
expect(s[to]()).toEqual(expected);
|
||||
});
|
||||
|
||||
testEncode('toHex', mockU32TextMapHexString);
|
||||
testEncode('toJSON', mockU32TextMapObject);
|
||||
testEncode('toU8a', mockU32TextMapUint8Array);
|
||||
testEncode('toString', mockU32TextMapString);
|
||||
});
|
||||
|
||||
describe('encoding muple values', (): void => {
|
||||
const testEncode = (to: CodecTo, expected: any): void =>
|
||||
it(`can encode ${to}`, (): void => {
|
||||
const s = new BTreeMap(Text, U32, mockU32U32Map);
|
||||
expect(s[to]()).toEqual(expected);
|
||||
});
|
||||
|
||||
testEncode('toHex', mockU32U32MapHexString);
|
||||
testEncode('toJSON', mockU32U32MapObject);
|
||||
testEncode('toU8a', mockU32U32MapUint8Array);
|
||||
testEncode('toString', mockU32U32MapString);
|
||||
});
|
||||
|
||||
it('decodes null', (): void => {
|
||||
expect(
|
||||
new (
|
||||
BTreeMap.with(Text, U32)
|
||||
)(null).toString()
|
||||
).toEqual('{}');
|
||||
});
|
||||
|
||||
it('decodes within more complicated types', (): void => {
|
||||
const s = new Struct({
|
||||
placeholder: U32,
|
||||
value: BTreeMap.with(Text, U32)
|
||||
});
|
||||
s.set('value', new BTreeMap(Text, U32, mockU32TextMap));
|
||||
expect(s.toString()).toBe('{"placeholder":0,"value":{"bazzing":69}}');
|
||||
});
|
||||
|
||||
it('throws when it cannot decode', (): void => {
|
||||
expect(
|
||||
(): BTreeMap<Text, U32> => new (
|
||||
BTreeMap.with(Text, U32)
|
||||
)('ABC')
|
||||
).toThrowError(/BTreeMap: cannot decode type/);
|
||||
});
|
||||
|
||||
it('correctly encodes length', (): void => {
|
||||
expect(
|
||||
new (
|
||||
BTreeMap.with(Text, U32))(mockU32TextMap).encodedLength
|
||||
).toEqual(13);
|
||||
});
|
||||
|
||||
it('generates sane toRawTypes', (): void => {
|
||||
expect(new (BTreeMap.with(Text, U32))().toRawType()).toBe('BTreeMap<Text,u32>');
|
||||
expect(new (BTreeMap.with(Text, Text))().toRawType()).toBe('BTreeMap<Text,Text>');
|
||||
expect(new (BTreeMap.with(Text, Struct.with({ a: U32, b: Text })))().toRawType())
|
||||
.toBe('BTreeMap<Text,{"a":"u32","b":"Text"}>');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2017-2019 @polkadot/types authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import Compact from './Compact';
|
||||
import U8a from './U8a';
|
||||
import { compareMap, decodeU8a } from './utils';
|
||||
import { AnyJson, Constructor, Codec, IHash } from '../types';
|
||||
import { isHex, hexToU8a, isU8a, u8aConcat, u8aToHex, u8aToU8a } from '@polkadot/util';
|
||||
import { blake2AsU8a } from '@polkadot/util-crypto';
|
||||
|
||||
export default class BTreeMap<K extends Codec = Codec, V extends Codec = Codec> extends Map<K, V> implements Codec {
|
||||
protected _key: Constructor<K>
|
||||
protected _val: Constructor<V>
|
||||
|
||||
constructor (keyT: Constructor<K>, valueT: Constructor<V>, rawValue: any) {
|
||||
super(BTreeMap.decodeBTreeMap(keyT, valueT, rawValue));
|
||||
this._key = keyT;
|
||||
this._val = valueT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode input to pass into constructor.
|
||||
*
|
||||
* @param keyConstructor - Type of the map key
|
||||
* @param valueConstructor - Type of the map value
|
||||
* @param value - Value to decode, one of:
|
||||
* - null
|
||||
* - undefined
|
||||
* - hex
|
||||
* - Uint8Array
|
||||
* - Map<any, any>, where both key and value types are either
|
||||
* constructors or decodeable values for their types.
|
||||
* @param jsonMap
|
||||
*/
|
||||
public static decodeBTreeMap<K extends Codec = Codec, V extends Codec = Codec> (
|
||||
KeyConstructor: Constructor<K>,
|
||||
ValueConstructor: Constructor<V>,
|
||||
value: Uint8Array | string | Map<any, any>): Map<K, V> {
|
||||
if (!value) {
|
||||
return new Map<K, V>();
|
||||
}
|
||||
|
||||
// Convert hex values to UA8 and re-run the function
|
||||
if (isHex(value)) {
|
||||
return BTreeMap.decodeBTreeMap(
|
||||
KeyConstructor,
|
||||
ValueConstructor,
|
||||
hexToU8a(value)
|
||||
);
|
||||
}
|
||||
|
||||
// Values already in U8A can be processed
|
||||
if (isU8a(value)) {
|
||||
return BTreeMap.decodeBTreeMapFromU8a<K, V>(KeyConstructor, ValueConstructor, u8aToU8a(value));
|
||||
}
|
||||
|
||||
// Try to handle map types
|
||||
if (value instanceof Map) {
|
||||
return BTreeMap.decodeBTreeMapFromMap<K, V>(KeyConstructor, ValueConstructor, value);
|
||||
}
|
||||
|
||||
throw new Error('BTreeMap: cannot decode type');
|
||||
}
|
||||
|
||||
public static decodeBTreeMapFromU8a<K extends Codec = Codec, V extends Codec = Codec> (
|
||||
KeyConstructor: Constructor<K>,
|
||||
ValueConstructor: Constructor<V>,
|
||||
u8a: Uint8Array): Map<K, V> {
|
||||
const output = new Map<K, V>();
|
||||
const [offset, length] = Compact.decodeU8a(u8a);
|
||||
|
||||
const types = [];
|
||||
for (let i = 0; i < length.toNumber(); i++) {
|
||||
types.push(KeyConstructor, ValueConstructor);
|
||||
}
|
||||
|
||||
const values = decodeU8a(u8a.subarray(offset), types);
|
||||
for (let i = 0; i < values.length; i += 2) {
|
||||
output.set(values[i] as K, values[i + 1] as V);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public static decodeBTreeMapFromMap<K extends Codec = Codec, V extends Codec = Codec> (
|
||||
KeyConstructor: Constructor<K>,
|
||||
ValueConstructor: Constructor<V>,
|
||||
value: Map<any, any>): Map<K, V> {
|
||||
const output = new Map<K, V>();
|
||||
value.forEach((v: any, k: any) => {
|
||||
let key, val;
|
||||
try {
|
||||
key = (k instanceof KeyConstructor) ? k : new KeyConstructor(k);
|
||||
val = (v instanceof ValueConstructor) ? v : new ValueConstructor(v);
|
||||
} catch (error) {
|
||||
console.error('Failed to decode BTreeMap key or value:', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
output.set(key, val);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
public static with<K extends Codec, V extends Codec> (k: Constructor<K>, v: Constructor<V>): Constructor<BTreeMap<K, V>> {
|
||||
return class extends BTreeMap<K, V> {
|
||||
constructor (value?: any) {
|
||||
super(k, v, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description The length of the value when encoded as a Uint8Array
|
||||
*/
|
||||
public get encodedLength (): number {
|
||||
let len = Compact.encodeU8a(this.size).length;
|
||||
this.forEach((v: V, k: K) => {
|
||||
len += v.encodedLength + k.encodedLength;
|
||||
});
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns a hash of the value
|
||||
*/
|
||||
public get hash (): IHash {
|
||||
return new U8a(blake2AsU8a(this.toU8a(), 256));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Checks if the value is an empty value
|
||||
*/
|
||||
public get isEmpty (): boolean {
|
||||
return this.size === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Compares the value of the input to see if there is a match
|
||||
*/
|
||||
public eq (other?: any): boolean {
|
||||
return compareMap(this, other);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns a hex string representation of the value. isLe returns a LE (number-only) representation
|
||||
*/
|
||||
public toHex (): string {
|
||||
return u8aToHex(this.toU8a());
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Converts the Object to JSON, typically used for RPC transfers
|
||||
*/
|
||||
public toJSON (): AnyJson {
|
||||
const json: any = {};
|
||||
this.forEach((v: V, k: K) => {
|
||||
json[k.toString()] = v.toJSON();
|
||||
});
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns the base runtime type name for this instance
|
||||
*/
|
||||
public toRawType (): string {
|
||||
return `BTreeMap<${new this._key().toRawType()},${new this._val().toRawType()}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns the string representation of the value
|
||||
*/
|
||||
public toString (): string {
|
||||
return JSON.stringify(this.toJSON());
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Encodes the value as a Uint8Array as per the SCALE specifications
|
||||
* @param isBare true when the value has none of the type-specific prefixes (internal)
|
||||
*/
|
||||
public toU8a (isBare?: boolean): Uint8Array {
|
||||
const encoded = new Array<Uint8Array>();
|
||||
|
||||
if (!isBare) {
|
||||
encoded.push(Compact.encodeU8a(this.size));
|
||||
}
|
||||
|
||||
this.forEach((v: V, k: K) => {
|
||||
encoded.push(k.toU8a(isBare), v.toU8a(isBare));
|
||||
});
|
||||
|
||||
return u8aConcat(...encoded);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
// others, so there _should_ not be need for direct use)
|
||||
|
||||
// These are the base codec types, generally used for construction
|
||||
export { default as BTreeMap } from './BTreeMap';
|
||||
export { default as Compact } from './Compact';
|
||||
// export { default as Date } from './Date';
|
||||
export { default as Enum } from './Enum';
|
||||
|
||||
Reference in New Issue
Block a user