From acfd856b25fa5f126c81f9bd514341540af565cd Mon Sep 17 00:00:00 2001 From: Paul M Fox Date: Mon, 14 Oct 2019 12:10:04 +0300 Subject: [PATCH] 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 * Update packages/types/src/codec/BTreeMap.ts Co-Authored-By: Jaco Greeff * Update packages/types/src/codec/BTreeMap.spec.ts Co-Authored-By: Jaco Greeff * added copyright; logic fixes * added BTreeMap in alphabetical order * better unit tests multiple value examples taken from scale codec repo * lint fix --- packages/types/src/codec/BTreeMap.spec.ts | 112 +++++++++++++ packages/types/src/codec/BTreeMap.ts | 195 ++++++++++++++++++++++ packages/types/src/codec/index.ts | 1 + 3 files changed, 308 insertions(+) create mode 100644 packages/types/src/codec/BTreeMap.spec.ts create mode 100644 packages/types/src/codec/BTreeMap.ts diff --git a/packages/types/src/codec/BTreeMap.spec.ts b/packages/types/src/codec/BTreeMap.spec.ts new file mode 100644 index 0000000000..e694d0f0ce --- /dev/null +++ b/packages/types/src/codec/BTreeMap.spec.ts @@ -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(); +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(); +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 => 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'); + expect(new (BTreeMap.with(Text, Text))().toRawType()).toBe('BTreeMap'); + expect(new (BTreeMap.with(Text, Struct.with({ a: U32, b: Text })))().toRawType()) + .toBe('BTreeMap'); + }); +}); diff --git a/packages/types/src/codec/BTreeMap.ts b/packages/types/src/codec/BTreeMap.ts new file mode 100644 index 0000000000..e8caede36e --- /dev/null +++ b/packages/types/src/codec/BTreeMap.ts @@ -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 extends Map implements Codec { + protected _key: Constructor + protected _val: Constructor + + constructor (keyT: Constructor, valueT: Constructor, 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, where both key and value types are either + * constructors or decodeable values for their types. + * @param jsonMap + */ + public static decodeBTreeMap ( + KeyConstructor: Constructor, + ValueConstructor: Constructor, + value: Uint8Array | string | Map): Map { + if (!value) { + return new Map(); + } + + // 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(KeyConstructor, ValueConstructor, u8aToU8a(value)); + } + + // Try to handle map types + if (value instanceof Map) { + return BTreeMap.decodeBTreeMapFromMap(KeyConstructor, ValueConstructor, value); + } + + throw new Error('BTreeMap: cannot decode type'); + } + + public static decodeBTreeMapFromU8a ( + KeyConstructor: Constructor, + ValueConstructor: Constructor, + u8a: Uint8Array): Map { + const output = new Map(); + 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 ( + KeyConstructor: Constructor, + ValueConstructor: Constructor, + value: Map): Map { + const output = new Map(); + 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: Constructor, v: Constructor): Constructor> { + return class extends BTreeMap { + 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(); + + 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); + } +} diff --git a/packages/types/src/codec/index.ts b/packages/types/src/codec/index.ts index 4b3f18dd4c..9672f62c38 100644 --- a/packages/types/src/codec/index.ts +++ b/packages/types/src/codec/index.ts @@ -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';