Intelligent constructor (Part 3) (#294)

* Fix Metadata

* Fix metadata

* Use constructor type

* Fix EnumType

* Fix EnumType

* Fix Tuple

* Remove some froms

* Fix Struct

* Fix Bft test

* Remove some froms

* Small fixes

* Fix EnumType

* Add blank lines

* Struct allow jsonKey or without

* Add hack

* Update hack test

* Revert some stuff

* Revert to from again

* Fix tests
This commit is contained in:
Amaury Martiny
2018-10-15 14:29:20 +02:00
committed by Jaco Greeff
parent b0d70b0435
commit 8de9ea8878
15 changed files with 185 additions and 77 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ const createRuntimeFunction = (method: string, key: string, { documentation, typ
{
documentation: new Vector(Text, [documentation]),
modifier: new StorageFunctionModifier().fromJSON(0),
type: new StorageFunctionType(0, type),
type: new StorageFunctionType(type, 0),
toJSON: (): any =>
key
} as StorageFunctionMetadata,
+1 -1
View File
@@ -36,7 +36,7 @@ describe('BftHashSignature', () => {
describe('Justification', () => {
const just = new Justification({
round: 32,
round: 32, // We get `round_number` from JSON, but both `round` and `round_number` works
hash: '0xabcd',
signatures: [
{ authorityId: '0x1234', signature: '0x5678' },
+2 -2
View File
@@ -21,12 +21,12 @@ export class Usurped extends Hash {
}
export default class ExtrinsicStatus extends EnumType<Finalised | Usurped | Broadcast | Dropped> {
constructor (index?: number) {
constructor (value: any, index?: number) {
super([
Finalised,
Usurped,
Broadcast,
Dropped
], index);
], value, index);
}
}
+26 -3
View File
@@ -4,6 +4,9 @@
import { AnyNumber } from './types';
import hexToU8a from '@polkadot/util/hex/toU8a';
import isHex from '@polkadot/util/is/hex';
import isU8a from '@polkadot/util/is/u8a';
import toU8a from '@polkadot/util/u8a/toU8a';
import Base from './codec/Base';
@@ -219,11 +222,11 @@ export class StorageFunctionType$Map extends Struct {
}
export class StorageFunctionType extends EnumType<Type | StorageFunctionType$Map> {
constructor (index?: number, value?: any) {
constructor (value?: any, index?: number) {
super([
Type,
StorageFunctionType$Map
], index, value);
], value, index);
}
get isMap (): boolean {
@@ -324,7 +327,27 @@ export default class RuntimeMetadata extends Struct {
outerEvent: OuterEventMetadata,
modules: Vector.with(RuntimeModuleMetadata),
outerDispatch: OuterDispatchMetadata
}, value);
}, RuntimeMetadata.decodeMetadata(value));
}
static decodeMetadata (value: any): object | Uint8Array {
if (isHex(value)) {
return RuntimeMetadata.decodeMetadata(hexToU8a(value));
} else if (isU8a(value)) {
// HACK 13 Oct 2018 - For current running BBQ nodes, Metadata is not properly
// encoded, it does not have a length prefix. For latest substrate master, it
// is properly encoded. Here we pull the prefix, check it agianst the length -
// if matches, then we have the length, otherwise we assume it is an older node
// and use the whole buffer
const [offset, length] = Compact.decodeU8a(value, DEFAULT_LENGTH_BITS);
return value.length === (offset + length.toNumber())
? value.subarray(offset)
: value;
}
// Decode as normal struct
return value;
}
// We receive this as an Array<number> in the JSON output from the Node. Convert
+4 -4
View File
@@ -55,11 +55,11 @@ export class BftDoubleCommit extends BftAtReport {
}
export class MisbehaviorKind extends EnumType<BftDoublePrepare | BftDoubleCommit> {
constructor (index: number = 0x11, value?: BftAtReportValue) {
constructor (value?: BftAtReportValue, index?: number) {
super({
0x11: BftDoublePrepare,
0x12: BftDoubleCommit
}, index, value);
}, value, index);
}
}
@@ -73,8 +73,8 @@ export default class MisbehaviorReport extends Struct {
constructor (value?: MisbehaviorReportValue) {
super({
parentHash: Hash,
parentNumber: BlockNumber,
target: AuthorityId,
parentNumber: BlockNumber,
target: AuthorityId,
misbehavior: MisbehaviorKind
}, value);
}
+7 -1
View File
@@ -92,7 +92,7 @@ describe('Compact', () => {
it('has the correct byteLength for constructor values (default)', () => {
expect(
new Compact(0xfffffff9).byteLength()
new Compact(0xfffffff9, 64).byteLength()
).toEqual(9);
});
@@ -107,4 +107,10 @@ describe('Compact', () => {
new Compact().fromU8a(new Uint8Array([254, 255, 3, 0])).raw
).toEqual(new BN(0xffff));
});
it('correctly adds the length prefix', () => {
expect(
Compact.addLengthPrefix(Uint8Array.from([12, 13]))
).toEqual(Uint8Array.from([2 << 2, 12, 13]));
});
});
+27 -12
View File
@@ -5,10 +5,12 @@
import BN from 'bn.js';
import bnToBn from '@polkadot/util/bn/toBn';
import bnToU8a from '@polkadot/util/bn/toU8a';
import isU8a from '@polkadot/util/is/u8a';
import u8aConcat from '@polkadot/util/u8a/concat';
import u8aToBn from '@polkadot/util/u8a/toBn';
import toU8a from '@polkadot/util/u8a/toU8a';
import { AnyNumber, AnyU8a } from '../types';
import UInt, { UIntBitLength } from './UInt';
export const DEFAULT_LENGTH_BITS = 32;
@@ -33,6 +35,31 @@ const MAX_U32 = new BN(2).pow(new BN(32 - 2)).subn(1);
//
// Note: we use *LOW BITS* of the LSB in LE encoding to encode the 2 bit key.
export default class Compact extends UInt {
constructor (value?: AnyU8a | AnyNumber, bitLength: UIntBitLength = DEFAULT_LENGTH_BITS, isHexJson: boolean = false) {
super(Compact.decodeCompact(value, bitLength), bitLength, isHexJson);
}
/**
* Prepend a Uint8Array with its compact length.
*
* @param u8a - The Uint8Array to be prefixed
*/
static addLengthPrefix (u8a: Uint8Array, bitLength: UIntBitLength = DEFAULT_LENGTH_BITS): Uint8Array {
return u8aConcat(
Compact.encodeU8a(u8a.length, bitLength),
u8a
);
}
static decodeCompact (value: AnyU8a | AnyNumber | undefined, bitLength: UIntBitLength): any {
if (isU8a(value)) {
const [, length] = Compact.decodeU8a(value, bitLength);
return length;
}
return value as any;
}
static decodeU8a (_input: Uint8Array | string, bitLength: UIntBitLength): [number, BN] {
const input = toU8a(_input);
const flag = input[0] & 0b11;
@@ -50,18 +77,6 @@ export default class Compact extends UInt {
return [byteLength + 1, u8aToBn(input.subarray(1, 1 + byteLength), true)];
}
/**
* Prepend a Uint8Array with its compact length.
*
* @param u8a - The Uint8Array to be prefixed
*/
static addLengthPrefix (u8a: Uint8Array): Uint8Array {
return u8aConcat(
Compact.encodeU8a(u8a.length, DEFAULT_LENGTH_BITS),
u8a
);
}
static encodeU8a (_value: UInt | BN | number, bitLength: UIntBitLength): Uint8Array {
const value = _value instanceof UInt
? _value.toBn()
+46 -7
View File
@@ -2,13 +2,17 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import isNumber from '@polkadot/util/is/number';
import isObject from '@polkadot/util/is/object';
import isU8a from '@polkadot/util/is/u8a';
import isUndefined from '@polkadot/util/is/undefined';
import Base from './Base';
import { Constructor } from '../types';
type TypesArray = Array<{ new(value?: any): Base }>;
type TypesArray = Array<Constructor<Base>>;
type TypesDef = {
[index: number]: { new(value?: any): Base }
[index: number]: Constructor<Base>
} | TypesArray;
// This implements an enum, that based on the value wraps a different type. It is effectively an
@@ -18,14 +22,16 @@ type TypesDef = {
// - As per Enum, actually use TS enum
// - It should rather probably extend Enum instead of copying code
// - There doesn't actually seem to be a way to get to the actual determined/wrapped value
export default class EnumType <T> extends Base<Base<T>> {
export default class EnumType<T> extends Base<Base<T>> {
private _Types: TypesArray;
private _index: number;
private _indexes: Array<number>;
constructor (def: TypesDef, index?: number | EnumType<T>, value?: any) {
constructor (def: TypesDef, value?: any, index?: number | EnumType<T>) {
const decoded = EnumType.decodeEnumType(def, value, index);
super(
new (Object.values(def)[0])()
decoded.value
);
this._Types = Array.isArray(def)
@@ -34,9 +40,42 @@ export default class EnumType <T> extends Base<Base<T>> {
this._indexes = Object.keys(def).map((index) =>
parseInt(index, 10)
);
this._index = this._indexes[0];
this.setValue(index, value);
this._index = this._indexes.indexOf(decoded.index) || 0;
}
static decodeEnumType<T> (def: TypesDef, value?: any, index?: number | EnumType<T>): { index: number, value: any } {
// If `index` is set, we parse it.
if (index instanceof EnumType) {
return { index: index._index, value: new def[index._index](index.raw) };
}
if (isNumber(index)) {
return { index, value: new def[index](value) };
}
// Or else, we just look at `value`
if (isU8a(value)) {
return { index: value[0], value: new def[value[0]](value.subarray(1)) };
} else if (isNumber(value) && !isUndefined(def[value])) {
return { index: value, value: new def[value]() };
} else if (isObject(value)) {
// JSON comes in the form of { "<type (lowercased)>": "<value for type>" }, here we
// additionally force to lower to ensure forward compat
const key = Object.keys(value)[0];
const lowerKey = key.toLowerCase();
const index = Object
.keys(def)
.find((k) => def[+k].name.toLowerCase() === lowerKey);
if (isUndefined(index)) {
throw new Error('Unable to reliably map input on JSON');
}
return { index: +index, value: new def[+index](value[key]) };
}
// Worst-case scenario, return this
return { index: 0, value: new (Object.values(def)[0])() };
}
get type (): string {
+10 -4
View File
@@ -7,6 +7,8 @@ import isU8a from '@polkadot/util/is/u8a';
import isUndefined from '@polkadot/util/is/undefined';
import Base from './Base';
import { Constructor } from '../types';
import Null from '../Null';
// An Option is an optional field. Basically the first byte indicates that there is
// is value to follow. If the byte is `1` there is an actual value. So the Option
@@ -15,7 +17,7 @@ import Base from './Base';
export default class Option<T> extends Base<Base<T>> {
private _isEmpty: boolean;
constructor (Type: { new(value?: any): Base<T> }, value?: any) {
constructor (Type: Constructor<Base<T>>, value?: any) {
super(
Option.decodeOption(Type, value)
);
@@ -23,8 +25,12 @@ export default class Option<T> extends Base<Base<T>> {
this._isEmpty = isNull(value) || isUndefined(value);
}
static decodeOption<O> (Type: { new(value?: any): Base<O> }, value?: any): Base<O> {
static decodeOption<O> (Type: Constructor<Base<O>>, value?: any): Base {
if (isU8a(value)) {
if (value[0] === 0) {
return new Null();
}
return new Type(value.subarray(1));
}
@@ -32,10 +38,10 @@ export default class Option<T> extends Base<Base<T>> {
isNull(value) || isUndefined(value)
? undefined
: value
);
);
}
static with<O> (Type: { new(value?: any): Base<O> }): { new(value?: any): Option<O> } {
static with<O> (Type: Constructor<Base<O>>): Constructor<Option<O>> {
return class extends Option<O> {
constructor (value?: any) {
super(Type, value);
+18 -12
View File
@@ -7,9 +7,10 @@ import isHex from '@polkadot/util/is/hex';
import isObject from '@polkadot/util/is/object';
import isU8a from '@polkadot/util/is/u8a';
import u8aConcat from '@polkadot/util/u8a/concat';
import u8aToU8a from '@polkadot/util/u8a/toU8a';
import toU8a from '@polkadot/util/u8a/toU8a';
import Base from './Base';
import { Constructor } from '../types';
// A Struct defines an Object with key/values - where the values are Base<T> values. It removes
// a lot of repetition from the actual coding, define a structure type, pass it the key/Base<T>
@@ -18,7 +19,7 @@ import Base from './Base';
// it needs to decoded in the specific defined order.
export default class Struct<
// The actual Class structure, i.e. key -> Class
S = { [index: string]: { new(...value: Array<any>): Base } },
S = { [index: string]: Constructor<Base> },
// internal type, instance of classes mapped by key
T = { [K in keyof S]: Base },
// input values, mapped by key can be anything (construction)
@@ -31,7 +32,7 @@ export default class Struct<
constructor (Types: S, value: V | Array<any> = {} as V, jsonMap: Map<keyof S, string> = new Map()) {
super(
Struct.decodeStruct(Types, value)
Struct.decodeStruct(Types, value, jsonMap)
);
this._jsonMap = jsonMap;
@@ -45,11 +46,13 @@ export default class Struct<
}, {} as E);
}
static decodeStruct<S, V, T> (Types: S, value: any): T {
static decodeStruct<S, V, T> (Types: S, value: any, jsonMap: Map<keyof S, string>): T {
// l.debug(() => ['Struct.decode', { Types, value }]);
if (isHex(value)) {
return Struct.decodeStruct(Types, hexToU8a(value as string));
return Struct.decodeStruct(Types, hexToU8a(value as string), jsonMap);
} else if (Array.isArray(value)) {
return Struct.decodeStruct(Types, toU8a(value), jsonMap);
} else if (!value) {
return {} as T;
}
@@ -61,6 +64,10 @@ export default class Struct<
return Object
.keys(Types)
.reduce((raw: T, key) => {
// The key in the JSON can be snake_case (or other cases), but in our
// Types, result or any other maps, it's camelCase
const jsonKey = (jsonMap.get(key as any) && !value[key]) ? jsonMap.get(key as any) : key;
if (isU8a(value)) {
// @ts-ignore FIXME See below
raw[key] = new Types[key](
@@ -72,11 +79,10 @@ export default class Struct<
// Move the currentIndex forward
// @ts-ignore FIXME See below
currentIndex += raw[key].byteLength();
// @ts-ignore FIXME See below
} else if (value[key] instanceof Types[key]) {
} else if (value[jsonKey] instanceof Types[key]) {
// @ts-ignore FIXME See below
raw[key] = value[key];
raw[key] = value[jsonKey];
} else if (isObject(value)) {
// @ts-ignore FIXME Ok, something weird is going on here or I just don't get it...
// it works, so ignore the checker, although it drives me batty. (It started when
@@ -84,7 +90,7 @@ export default class Struct<
// does backfire here, but works externally.)
raw[key] = new Types[key](
// @ts-ignore FIXME
value[key]
value[jsonKey]
);
} else {
// @ts-ignore FIXME
@@ -96,8 +102,8 @@ export default class Struct<
}
static with<
S = { [index: string]: { new(value?: any): Base } }
> (Types: S): { new(value?: any): Struct<S> } {
S = { [index: string]: Constructor<Base> }
> (Types: S): Constructor<Struct<S>> {
return class extends Struct<S> {
constructor (value?: any, jsonMap?: Map<keyof S, string>) {
super(Types, value, jsonMap);
@@ -117,7 +123,7 @@ export default class Struct<
fromJSON (input: any): Struct<S, T, V, E> {
if (isHex(input) || isU8a(input)) {
return this.fromU8a(u8aToU8a(input));
return this.fromU8a(toU8a(input));
}
// null & undefined yields empty
+19
View File
@@ -34,6 +34,25 @@ describe('Tuple', () => {
});
it('initialises via JSON array', () => {
const test = new (Tuple.with({
a: Text,
b: U32,
c: Text
}))([
'bazzing', 32
]);
expect(
test.toJSON()
).toEqual([
'bazzing',
32,
''
]);
});
// FIXME Remove fromJSON
it('fromJSON still works', () => {
const test = new (Tuple.with({
a: Text,
b: U32,
+16 -25
View File
@@ -2,14 +2,10 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { AnyU8a } from '../types';
import hexToU8a from '@polkadot/util/hex/toU8a';
import isHex from '@polkadot/util/is/hex';
import isString from '@polkadot/util/is/string';
import isU8a from '@polkadot/util/is/u8a';
import toU8a from '@polkadot/util/u8a/toU8a';
import { AnyU8a, Constructor } from '../types';
import Base from './Base';
import Struct from './Struct';
@@ -18,7 +14,7 @@ import Struct from './Struct';
// while the U8a encoding is handled in the same way as a Struct
export default class Tuple<
// S & T definitions maps to what we have in Struct (naming documented there)
S = { [index: string]: { new(value?: any): Base } },
S = { [index: string]: Constructor<Base> },
T = { [K in keyof S]: Base },
V = { [K in keyof S]: any }
> extends Struct<S, T, V> {
@@ -26,30 +22,25 @@ export default class Tuple<
super(Types, Tuple.decodeTuple(Types, value), jsonMap);
}
static decodeTuple<S, V> (Types: S, _value: V | AnyU8a): V {
if (!isU8a(_value) && !isString(_value) && !Array.isArray(_value)) {
return _value as V;
static decodeTuple<S, V> (Types: S, value: V | AnyU8a): V {
// If the input is an array, we convert it to a map
if (Array.isArray(value)) {
return Object
.keys(Types)
.reduce((result, key, index) => {
// @ts-ignore FIXME these types are a headache
result[key] = value[index];
return result;
}, {} as V);
}
const value = toU8a(_value);
let offset = 0;
return Object
.keys(Types)
.reduce((result: V, key) => {
// @ts-ignore
result[key] = new Types[key]().fromU8a(value.subarray(offset));
// @ts-ignore
offset += result[key].byteLength();
return result;
}, {} as V);
// Or else, just decode like a normal Struct
return value as V;
}
static with<
S = { [index: string]: { new(value?: any): Base } }
> (Types: S): { new(value?: any): Tuple<S> } {
S = { [index: string]: Constructor<Base> }
> (Types: S): Constructor<Tuple<S>> {
return class extends Tuple<S> {
constructor (value?: any) {
super(Types, value);
+1 -1
View File
@@ -13,7 +13,7 @@ describe('Vector', () => {
let array;
beforeEach(() => {
array = new Vector(Text, [ '1', '23', '345', '4567', new Text('56789') ]);
array = new Vector(Text, ['1', '23', '345', '4567', new Text('56789')]);
Method.injectExtrinsics(extrinsics);
});
+5 -4
View File
@@ -7,6 +7,7 @@ import toU8a from '@polkadot/util/u8a/toU8a';
import Base from './Base';
import Compact, { DEFAULT_LENGTH_BITS } from './Compact';
import { Constructor } from '../types';
// This manages codec arrays. Intrernally it keeps track of the length (as decoded) and allows
// construction with the passed `Type` in the constructor. It aims to be an array-like structure,
@@ -16,9 +17,9 @@ import Compact, { DEFAULT_LENGTH_BITS } from './Compact';
export default class Vector<
T extends Base
> extends Base<Array<T>> {
private _Type: { new(value?: any): T };
private _Type: Constructor<T>;
constructor (Type: { new(value?: any): T }, value: Uint8Array | string | Array<any> = [] as Array<any>) {
constructor (Type: Constructor<T>, value: Uint8Array | string | Array<any> = [] as Array<any>) {
super(
Vector.decode(Type, value)
);
@@ -26,7 +27,7 @@ export default class Vector<
this._Type = Type;
}
static decode<T> (Type: { new(value?: any): T }, value: Uint8Array | string | Array<any>): Array<T> {
static decode<T extends Base> (Type: Constructor<T>, value: Uint8Array | string | Array<any>): Array<T> {
if (Array.isArray(value)) {
return value.map((entry) =>
entry instanceof Type
@@ -55,7 +56,7 @@ export default class Vector<
return result;
}
static with<O extends Base> (Type: { new(value?: any): O }): { new(value?: any): Vector<O> } {
static with<O extends Base> (Type: Constructor<O>): Constructor<Vector<O>> {
return class extends Vector<O> {
constructor (value?: Array<any>) {
super(Type, value);
+2
View File
@@ -12,3 +12,5 @@ export type AnyNumber = UInt | BN | Uint8Array | number | string;
export type AnyU8a = U8a | Uint8Array | Array<number> | string;
export type CodecTypes = keyof typeof Classes;
export type Constructor<T> = { new(value?: any): T };