Decoding cleanups (#1321)

* Decoding cleanups

* Split extractDef

* Extract struct types to string mapping
This commit is contained in:
Jaco Greeff
2019-08-18 16:42:17 +02:00
committed by GitHub
parent 41a013371a
commit 71d4a7e7f2
10 changed files with 302 additions and 308 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ export default abstract class AbstractInt extends BN implements Codec {
// This function returns a string, which will be passed in the BN
// constructor. It would be ideal to actually return a BN, but there's a
// bug: https://github.com/indutny/bn.js/issues/206.
if (isHex(value)) {
if (isHex(value, -1, true)) {
return hexToBn(value, { isLe: false, isNegative }).toString();
} else if (isU8a(value)) {
return AbstractInt.decodeAbstracIntU8a(value, bitLength, isNegative);
+3 -9
View File
@@ -5,7 +5,7 @@
import { AnyNumber, Codec, Constructor, InterfaceTypes } from '../types';
import BN from 'bn.js';
import { bnToBn, compactAddLength, compactFromU8a, compactStripLength, compactToU8a, hexToBn, isBn, isHex, isNumber, isString } from '@polkadot/util';
import { compactAddLength, compactFromU8a, compactStripLength, compactToU8a, isBn, isNumber, isString } from '@polkadot/util';
import { DEFAULT_BITLENGTH } from '@polkadot/util/compact/defaults';
import { typeToConstructor } from './utils';
@@ -59,14 +59,8 @@ export default class Compact<T extends CompactEncodable> extends Base<T> {
public static decodeCompact<T extends CompactEncodable> (Type: Constructor<T>, value: Compact<T> | AnyNumber): CompactEncodable {
if (value instanceof Compact) {
return new Type(value.raw);
} else if (isString(value)) {
return new Type(
isHex(value, -1, true)
? hexToBn(value)
: new BN(value, 10)
);
} else if (isNumber(value) || isBn(value)) {
return new Type(bnToBn(value));
} else if (isString(value) || isNumber(value) || isBn(value)) {
return new Type(value);
}
const [, _value] = Compact.decodeU8a(value, new Type(0).bitLength());
+275 -2
View File
@@ -2,6 +2,279 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import Enum from './EnumType';
import { AnyJson, Codec, Constructor, InterfaceTypes } from '../types';
export default Enum;
import { assert, hexToU8a, isHex, isNumber, isObject, isString, isU8a, isUndefined, stringCamelCase, stringUpperFirst, u8aConcat, u8aToHex } from '@polkadot/util';
import Null from '../primitive/Null';
import { mapToTypeMap } from './utils';
import Base from './Base';
import Struct from './Struct';
interface EnumConstructor<T = Codec> {
new(value?: any, index?: number): T;
}
type TypesDef = Record<string, Constructor>;
interface Decoded {
index: number;
value: Codec;
}
function extractDef (_def: Record<string, InterfaceTypes | Constructor> | string[]): { def: TypesDef; isBasic: boolean } {
if (!Array.isArray(_def)) {
const def = mapToTypeMap(_def);
const isBasic = !Object.values(def).some((type): boolean => type !== Null);
return {
def,
isBasic
};
}
return {
def: _def.reduce((def, key): TypesDef => {
def[key] = Null;
return def;
}, {} as unknown as TypesDef),
isBasic: true
};
}
/**
* @name Enum
* @description
* This implements an enum, that based on the value wraps a different type. It is effectively
* an extension to enum where the value type is determined by the actual index.
*/
// TODO:
// - As per Enum, actually use TS enum
// - It should rather probably extend Enum instead of copying code
export default class Enum extends Base<Codec> {
private _def: TypesDef;
private _index: number;
private _indexes: number[];
private _isBasic: boolean;
public constructor (def: Record<string, InterfaceTypes | Constructor> | string[], value?: any, index?: number | Enum) {
const defInfo = extractDef(def);
const decoded = Enum.decodeEnum(defInfo.def, value, index);
super(decoded.value);
this._def = defInfo.def;
this._isBasic = defInfo.isBasic;
this._indexes = Object.keys(defInfo.def).map((_, index): number => index);
this._index = this._indexes.indexOf(decoded.index) || 0;
}
private static decodeEnum (def: TypesDef, value?: any, index?: number | Enum): Decoded {
if (value instanceof Enum) {
return Enum.createValue(def, value._index, value.raw);
} else if (index instanceof Enum) {
return Enum.createValue(def, index._index, index.raw);
} else if (isNumber(index)) {
return Enum.createValue(def, index, value);
}
// Or else, we just look at `value`
return Enum.decodeViaValue(def, value);
}
private static decodeViaValue (def: TypesDef, value?: any): Decoded {
if (isU8a(value)) {
return Enum.createValue(def, value[0], value.subarray(1));
} else if (isNumber(value)) {
return Enum.createValue(def, value);
} else if (isString(value)) {
return Enum.decodeViaString(def, value.toString());
} else if (isObject(value)) {
const key = Object.keys(value)[0];
return Enum.createViaJSON(def, key, value[key]);
}
// Worst-case scenario, return the first with default
return Enum.createValue(def, 0);
}
private static decodeViaString (def: TypesDef, value: string): Decoded {
return isHex(value)
? Enum.decodeViaValue(def, hexToU8a(value))
: Enum.createViaJSON(def, value);
}
private static createViaJSON (def: TypesDef, key: string, value?: any): Decoded {
// JSON comes in the form of { "<type (lowercased)>": "<value for type>" }, here we
// additionally force to lower to ensure forward compat
const keys = Object.keys(def).map((k): string => k.toLowerCase());
const keyLower = key.toLowerCase();
const index = keys.indexOf(keyLower);
assert(index !== -1, `Cannot map Enum JSON, unable to find '${key}' in ${keys.join(', ')}`);
return Enum.createValue(def, index, value);
}
private static createValue (def: TypesDef, index: number = 0, value?: any): Decoded {
const Clazz = Object.values(def)[index];
assert(!isUndefined(Clazz), `Unable to create Enum via index ${index}, in ${Object.keys(def).join(', ')}`);
return {
index,
value: new Clazz(value)
};
}
public static with (Types: Record<string, InterfaceTypes | Constructor> | string[]): EnumConstructor<Enum> {
return class extends Enum {
public constructor (value?: any, index?: number) {
super(Types, value, index);
Object.keys(this._def).forEach((_key): void => {
const name = stringUpperFirst(stringCamelCase(_key.replace(' ', '_')));
const askey = `as${name}`;
const iskey = `is${name}`;
// do not clobber existing properties on the object
if (isUndefined((this as any)[iskey])) {
Object.defineProperty(this, iskey, {
enumerable: true,
get: (): boolean => this.type === _key
});
}
if (isUndefined((this as any)[askey])) {
Object.defineProperty(this, askey, {
enumerable: true,
get: (): Codec => {
assert((this as any)[iskey], `Cannot convert '${this.type}' via ${askey}`);
return this.value;
}
});
}
});
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return 1 + this.raw.encodedLength;
}
/**
* @description The index of the metadata value
*/
public get index (): number {
return this._index;
}
/**
* @description Checks if the Enum points to a [[Null]] type
*/
public get isNone (): boolean {
return this.isNull;
}
/**
* @description Checks if the Enum points to a [[Null]] type (deprecated, use isNone)
*/
public get isNull (): boolean {
return this.raw instanceof Null;
}
/**
* @description The name of the type this enum value represents
*/
public get type (): string {
return Object.keys(this._def)[this._index];
}
/**
* @description The value of the enum
*/
public get value (): Codec {
return this.raw;
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: any): boolean {
// cater for the case where we only pass the enum index
if (isNumber(other)) {
return this.toNumber() === other;
} else if (this._isBasic && isString(other)) {
return this.type === other;
}
// compare the actual wrapper value
return this.value.eq(other);
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): string {
return u8aToHex(this.toU8a());
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): AnyJson {
return this._isBasic
? this._index
: { [this.type]: this.raw.toJSON() };
}
/**
* @description Returns the number representation for the value
*/
public toNumber (): number {
return this._index;
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
const _enum = this._isBasic
? Object.keys(this._def)
: Struct.typesToMap(this._def);
return JSON.stringify({ _enum });
}
/**
* @description Returns the string representation of the value
*/
public toString (): string {
return this.isNull
? this.type
: 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 index = this._indexes[this._index];
return u8aConcat(
new Uint8Array([index]),
this.raw.toU8a(isBare)
);
}
}
-282
View File
@@ -1,282 +0,0 @@
// 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 { AnyJson, Codec, Constructor, InterfaceTypes } from '../types';
import { assert, hexToU8a, isHex, isNumber, isObject, isString, isU8a, isUndefined, stringCamelCase, stringUpperFirst, u8aConcat, u8aToHex } from '@polkadot/util';
import Null from '../primitive/Null';
import { mapToTypeMap } from './utils';
import Base from './Base';
interface EnumConstructor<T = Codec> {
new(value?: any, index?: number): T;
}
type TypesDef = Record<string, Constructor>;
interface Decoded {
index: number;
value: Codec;
}
/**
* @name Enum
* @description
* This implements an enum, that based on the value wraps a different type. It is effectively
* an extension to enum where the value type is determined by the actual index.
*/
// TODO:
// - As per Enum, actually use TS enum
// - It should rather probably extend Enum instead of copying code
export default class Enum extends Base<Codec> {
private _def: TypesDef;
private _index: number;
private _indexes: number[];
private _isBasic: boolean;
public constructor (def: Record<string, InterfaceTypes | Constructor> | string[], value?: any, index?: number | Enum) {
const defInfo = Enum.extractDef(def);
const decoded = Enum.decodeEnum(defInfo.def, value, index);
super(decoded.value);
this._def = defInfo.def;
this._isBasic = defInfo.isBasic;
this._indexes = Object.keys(defInfo.def).map((_, index): number => index);
this._index = this._indexes.indexOf(decoded.index) || 0;
}
private static extractDef (_def: Record<string, InterfaceTypes | Constructor> | string[]): { def: TypesDef; isBasic: boolean } {
if (!Array.isArray(_def)) {
const def = mapToTypeMap(_def);
const isBasic = !Object.values(def).some((type): boolean => type !== Null);
return {
def,
isBasic
};
}
return {
def: _def.reduce((def, key): TypesDef => {
def[key] = Null;
return def;
}, {} as unknown as TypesDef),
isBasic: true
};
}
private static decodeEnum (def: TypesDef, value?: any, index?: number | Enum): Decoded {
// If `index` is set, we parse it.
if (index instanceof Enum) {
return Enum.createValue(def, index._index, index.raw);
} else if (isNumber(index)) {
return Enum.createValue(def, index, value);
}
// Or else, we just look at `value`
return Enum.decodeViaValue(def, value);
}
private static decodeViaValue (def: TypesDef, value?: any): Decoded {
if (value instanceof Enum) {
return Enum.createValue(def, value._index, value.raw);
} else if (isU8a(value)) {
return Enum.createValue(def, value[0], value.subarray(1));
} else if (isNumber(value)) {
return Enum.createValue(def, value);
} else if (isString(value)) {
const _str = value.toString();
return isHex(_str)
? Enum.decodeViaValue(def, hexToU8a(_str))
: Enum.createViaJSON(def, _str);
} else if (isObject(value)) {
const key = Object.keys(value)[0];
return Enum.createViaJSON(def, key, value[key]);
}
// Worst-case scenario, return the first with default
return Enum.createValue(def, 0);
}
private static createViaJSON (def: TypesDef, key: string, value?: any): Decoded {
// JSON comes in the form of { "<type (lowercased)>": "<value for type>" }, here we
// additionally force to lower to ensure forward compat
const keys = Object.keys(def).map((k): string => k.toLowerCase());
const keyLower = key.toLowerCase();
const index = keys.indexOf(keyLower);
assert(index !== -1, `Cannot map Enum JSON, unable to find '${key}' in ${keys.join(', ')}`);
return Enum.createValue(def, index, value);
}
private static createValue (def: TypesDef, index: number = 0, value?: any): Decoded {
const Clazz = Object.values(def)[index];
assert(!isUndefined(Clazz), `Unable to create Enum via index ${index}, in ${Object.keys(def).join(', ')}`);
return {
index,
value: new Clazz(value)
};
}
public static with (Types: Record<string, InterfaceTypes | Constructor> | string[]): EnumConstructor<Enum> {
return class extends Enum {
public constructor (value?: any, index?: number) {
super(Types, value, index);
Object.keys(this._def).forEach((_key): void => {
const name = stringUpperFirst(stringCamelCase(_key.replace(' ', '_')));
const askey = `as${name}`;
const iskey = `is${name}`;
// do not clobber existing properties on the object
if (isUndefined((this as any)[iskey])) {
Object.defineProperty(this, iskey, {
enumerable: true,
get: (): boolean => this.type === _key
});
}
if (isUndefined((this as any)[askey])) {
Object.defineProperty(this, askey, {
enumerable: true,
get: (): Codec => {
assert((this as any)[iskey], `Cannot convert '${this.type}' via ${askey}`);
return this.value;
}
});
}
});
}
};
}
/**
* @description The length of the value when encoded as a Uint8Array
*/
public get encodedLength (): number {
return 1 + this.raw.encodedLength;
}
/**
* @description The index of the metadata value
*/
public get index (): number {
return this._index;
}
/**
* @description Checks if the Enum points to a [[Null]] type
*/
public get isNone (): boolean {
return this.isNull;
}
/**
* @description Checks if the Enum points to a [[Null]] type (deprecated, use isNone)
*/
public get isNull (): boolean {
return this.raw instanceof Null;
}
/**
* @description The name of the type this enum value represents
*/
public get type (): string {
return Object.keys(this._def)[this._index];
}
/**
* @description The value of the enum
*/
public get value (): Codec {
return this.raw;
}
/**
* @description Compares the value of the input to see if there is a match
*/
public eq (other?: any): boolean {
// cater for the case where we only pass the enum index
if (isNumber(other)) {
return this.toNumber() === other;
} else if (this._isBasic && isString(other)) {
return this.type === other;
}
// compare the actual wrapper value
return this.value.eq(other);
}
/**
* @description Returns a hex string representation of the value
*/
public toHex (): string {
return u8aToHex(this.toU8a());
}
/**
* @description Converts the Object to JSON, typically used for RPC transfers
*/
public toJSON (): AnyJson {
return this._isBasic
? this._index
: { [this.type]: this.raw.toJSON() };
}
/**
* @description Returns the number representation for the value
*/
public toNumber (): number {
return this._index;
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
const _enum = this._isBasic
? Object.keys(this._def)
: Object.entries(this._def).reduce((result, [key, Type]): Record<string, string> => {
result[key] = new Type().toRawType();
return result;
}, {} as unknown as Record<string, string>);
return JSON.stringify({ _enum });
}
/**
* @description Returns the string representation of the value
*/
public toString (): string {
return this.isNull
? this.type
: 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 index = this._indexes[this._index];
return u8aConcat(
new Uint8Array([index]),
this.raw.toU8a(isBare)
);
}
}
+7 -3
View File
@@ -40,14 +40,18 @@ export default class Option<T extends Codec> extends Base<T> {
} else if (isU8a(value)) {
// the isU8a check happens last in the if-tree - since the wrapped value
// may be an instance of it, so Type and Option checks go in first
return !value.length || value[0] === 0
? new Null()
: new Type(value.subarray(1));
return Option.decodeOptionU8a(Type, value);
}
return new Type(value);
}
private static decodeOptionU8a (Type: Constructor, value: Uint8Array): Codec {
return !value.length || value[0] === 0
? new Null()
: new Type(value.subarray(1));
}
public static with<O extends Codec> (Type: Constructor | InterfaceTypes): Constructor<Option<O>> {
return class extends Option<O> {
public constructor (value?: any) {
+9 -5
View File
@@ -234,16 +234,20 @@ export default class Struct<
}, {} as any);
}
public static typesToMap (Types: Record<string, Constructor>): Record<string, string> {
return Object.entries(Types).reduce((result, [key, Type]): Record<string, string> => {
result[key] = new Type().toRawType();
return result;
}, {} as unknown as Record<string, string>);
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return JSON.stringify(
Object.entries(this._Types).reduce((result, [key, Type]): Record<string, string> => {
result[key] = new Type().toRawType();
return result;
}, {} as unknown as Record<string, string>)
Struct.typesToMap(this._Types)
);
}
-1
View File
@@ -11,7 +11,6 @@
export { default as Compact } from './Compact';
// export { default as Date } from './Date';
export { default as Enum } from './Enum';
export { default as EnumType } from './EnumType';
export { default as Option } from './Option';
export { default as Set } from './Set';
export { default as Struct } from './Struct';
+5 -3
View File
@@ -13,15 +13,17 @@ function hasMismatch (a?: any, b?: any): boolean {
);
}
function notEntry (value: any): boolean {
return !Array.isArray(value) || value.length !== 2;
}
// NOTE These are used internally and when comparing objects, expects that
// when the second is an Map<string, Codec> that the first has to be as well
export default function compareMap (a: Map<any, any>, b?: any): boolean {
if (Array.isArray(b)) {
// equal number of entries and each entry in the array should match
return (a.size === b.length) && !b.some((entry): boolean =>
!Array.isArray(entry) ||
entry.length !== 2 ||
hasMismatch(a.get(entry[0]), entry[1])
notEntry(entry) || hasMismatch(a.get(entry[0]), entry[1])
);
} else if (b instanceof Map) {
return compareMap(a, [...b.entries()]);
@@ -15,14 +15,14 @@ export default {
}
},
RawBabePreDigestPrimary: {
authorityIndex: 'u32', // AuthorityIndex (also in aura, not same size there)
authorityIndex: 'u32', // AuthorityIndex (also in aura)
slotNumber: 'SlotNumber',
weight: 'BabeBlockWeight',
vrfOutput: 'H256', // should be '[u8; 32]' (generator support lacking here)
vrfProof: 'H256' // should be '[u8; 32]'
},
RawBabePreDigestSecondary: {
authorityIndex: 'u32', // AuthorityIndex (also in aura, not same size there)
authorityIndex: 'u32', // AuthorityIndex (also in aura)
slotNumber: 'SlotNumber',
weight: 'BabeBlockWeight'
},