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:
Paul M Fox
2019-10-14 11:10:04 +02:00
committed by Jaco Greeff
co-authored by Jaco Greeff
parent d5d8b59f8b
commit acfd856b25
3 changed files with 308 additions and 0 deletions
+112
View File
@@ -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"}>');
});
});
+195
View File
@@ -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);
}
}
+1
View File
@@ -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';