Add Result type (#1451)

* Add Result class

* Allow createType to operate on Result

* Formatting of Result type in interfaces (incl. use)

* Tests for Result

* Linting & createType seems to work, any final round of gaps?

* Bump metadata containing Result<Null,DispatchError>

* extractTypes handles Result (validation)

* Remove any from creation params

* Ok, give up trying to make `with` sane... it stays enum-like :(

* Move Enum protected decode member to file-level
This commit is contained in:
Jaco Greeff
2019-10-07 13:41:26 +02:00
committed by GitHub
parent 525b6bf4ad
commit cf07d4f743
17 changed files with 326 additions and 93 deletions
@@ -24,13 +24,14 @@ function extractTypes (types: string[]): any[] {
case TypeDefInfo.VecFixed:
return extractTypes([(decoded.ext as TypeDefExtVecFixed).type]);
case TypeDefInfo.Result:
case TypeDefInfo.Tuple:
return extractTypes(
(decoded.sub as TypeDef[]).map((sub): string => sub.type)
);
default:
throw new Error(`Uhandled: Unnable to create and validate type from ${type}`);
throw new Error(`Unhandled: Unable to create and validate type from ${type}`);
}
});
}
@@ -332,7 +332,7 @@
{
"name": "BatchExecuted",
"args": [
"Vec<DispatchResult>"
"Vec<Result<Null,DispatchError>>"
],
"documentation": []
}
File diff suppressed because one or more lines are too long
+60 -54
View File
@@ -44,6 +44,53 @@ function extractDef (_def: Record<string, InterfaceTypes | Constructor> | string
};
}
function createFromValue (def: TypesDef, index = 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)
};
}
function decodeFromJSON (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 createFromValue(def, index, value);
}
function decodeFromString (def: TypesDef, value: string): Decoded {
return isHex(value)
// eslint-disable-next-line @typescript-eslint/no-use-before-define
? decodeFromValue(def, hexToU8a(value))
: decodeFromJSON(def, value);
}
function decodeFromValue (def: TypesDef, value?: any): Decoded {
if (isU8a(value)) {
return createFromValue(def, value[0], value.subarray(1));
} else if (isNumber(value)) {
return createFromValue(def, value);
} else if (isString(value)) {
return decodeFromString(def, value.toString());
} else if (isObject(value)) {
const key = Object.keys(value)[0];
return decodeFromJSON(def, key, value[key]);
}
// Worst-case scenario, return the first with default
return createFromValue(def, 0);
}
/**
* @name Enum
* @description
@@ -78,59 +125,13 @@ export default class Enum extends Base<Codec> {
// NOTE We check the index path first, before looking at values - this allows treating
// the optional indexes before anything else, more-specific > less-specific
if (isNumber(index)) {
return Enum.createValue(def, index, value);
return createFromValue(def, index, value);
} else if (value instanceof Enum) {
return Enum.createValue(def, value._index, value.raw);
return createFromValue(def, value._index, value.raw);
}
// 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 = 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)
};
return decodeFromValue(def, value);
}
public static with (Types: Record<string, InterfaceTypes | Constructor> | string[]): EnumConstructor<Enum> {
@@ -246,15 +247,20 @@ export default class Enum extends Base<Codec> {
return this._index;
}
/**
* @description Returns a raw struct representation of the enum types
*/
protected toRawStruct (): string[] | Record<string, string> {
return this._isBasic
? Object.keys(this._def)
: Struct.typesToMap(this._def);
}
/**
* @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 });
return JSON.stringify({ _enum: this.toRawStruct() });
}
/**
+51
View File
@@ -0,0 +1,51 @@
// 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 { hexToString } from '@polkadot/util';
import { u32, Text } from '../primitive';
import { createType } from './create';
import Result from './Result';
describe('Result', (): void => {
const Type = Result.with({ Ok: u32, Error: Text });
it('has a sane toRawType representation', (): void => {
expect(new Type().toRawType()).toEqual('Result<u32,Text>');
});
it('decodes from a u8a (success)', (): void => {
const result = new Type(new Uint8Array([0, 1, 2, 3, 4]));
expect(result.isOk);
expect(result.asOk.toU8a()).toEqual(new Uint8Array([1, 2, 3, 4]));
expect(result.toHex()).toEqual('0x0001020304');
expect(result.toJSON()).toEqual({
Ok: 0x04030201
});
});
it('decodes from a u8a (error)', (): void => {
const result = new Type(new Uint8Array([1, 4 << 2, 100, 101, 102, 103]));
expect(result.isError);
expect(result.asError.toU8a()).toEqual(new Uint8Array([4 << 2, 100, 101, 102, 103]));
expect(result.toHex()).toEqual('0x011064656667');
expect(result.toJSON()).toEqual({
Error: hexToString('0x64656667')
});
});
it('decodes from a JSON representation', (): void => {
const result = new Type({ Error: 'error' });
expect(result.toHex()).toEqual('0x01146572726f72');
});
it('returns a proper raw typedef rom a built-in', (): void => {
expect(createType('DispatchResult').toRawType()).toEqual('Result<Null,Text>');
});
});
+77
View File
@@ -0,0 +1,77 @@
// 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 { Codec, Constructor, InterfaceTypes } from '../types';
import { assert } from '@polkadot/util';
import Enum from './Enum';
/**
* @name Result
* @description
* A Result maps to the Rust Result type, that can either wrap a success or error value
*/
export default class Result<O extends Codec, E extends Codec> extends Enum {
public constructor (Ok: Constructor<O> | InterfaceTypes, Error: Constructor<E> | InterfaceTypes, value?: any) {
// NOTE This is order-dependent, Ok (with index 0) needs to be first
super({ Ok, Error }, value);
}
public static with<O extends Codec, E extends Codec> (Types: { Ok: Constructor<O> | InterfaceTypes;Error: Constructor<E> | InterfaceTypes }): Constructor<Result<O, E>> {
return class extends Result<O, E> {
public constructor (value?: any) {
super(Types.Ok, Types.Error, value);
}
};
}
/**
* @description Returns the wrapper Error value (if isError)
*/
public get asError (): E {
assert(this.isError, 'Cannot extract Error value from Ok result, check isError first');
return this.value as E;
}
/**
* @description Returns the wrapper Ok value (if isOk)
*/
public get asOk (): O {
assert(this.isOk, 'Cannot extract Ok value from Error result, check isOk first');
return this.value as O;
}
/**
* @description Checks if the Result has no value
*/
public get isEmpty (): boolean {
return this.isOk && this.raw.isEmpty;
}
/**
* @description Checks if the Result wraps an Error value
*/
public get isError (): boolean {
return !this.isOk;
}
/**
* @description Checks if the Result wraps an Ok value
*/
public get isOk (): boolean {
return this.index === 0;
}
/**
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
const Types = this.toRawStruct() as { Ok: any; Error: any };
return `Result<${Types.Ok},${Types.Error}>`;
}
}
@@ -20,6 +20,12 @@ describe('createType', (): void => {
});
});
it('allows creation of a Result', (): void => {
expect(
createTypeUnsafe('Result<u32,Text>', ['0x011064656667']).toJSON()
).toEqual({ Error: 'defg' });
});
it('allows creation of a Tuple', (): void => {
expect(
createTypeUnsafe('(Balance,u32)', [[1234, 5678]]).toJSON()
@@ -11,6 +11,7 @@ import Compact from '../Compact';
import Enum from '../Enum';
import Linkage from '../Linkage';
import Option from '../Option';
import Result from '../Result';
import CodecSet from '../Set';
import Struct from '../Struct';
import Tuple from '../Tuple';
@@ -72,6 +73,13 @@ const infoMapping: Record<TypeDefInfo, (value: TypeDef) => Constructor> = {
[TypeDefInfo.Plain]: (value: TypeDef): Constructor =>
getTypeRegistry().getOrThrow(value.type, `Unable to find plain type for ${JSON.stringify(value)}`),
[TypeDefInfo.Result]: (value: TypeDef): Constructor => {
const [okDef, errorDef] = getSubDefArray(value);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return Result.with({ Ok: getTypeClass(okDef), Error: getTypeClass(errorDef) });
},
[TypeDefInfo.Set]: (value: TypeDef): Constructor => {
const result: Record<string, number> = {};
@@ -21,6 +21,63 @@ describe('getTypeDef', (): void => {
).toThrow(/Unable to find closing matching/);
});
it('maps empty tuples to Null', (): void => {
expect(
getTypeDef('()')
).toEqual({
info: TypeDefInfo.Plain,
type: 'Null'
});
});
it('properly decodes a Result<u32, Text>', (): void => {
expect(
getTypeDef('Result<u32, Text>')
).toEqual({
info: TypeDefInfo.Result,
type: 'Result<u32,Text>',
sub: [
{
info: TypeDefInfo.Plain,
type: 'u32'
},
{
info: TypeDefInfo.Plain,
type: 'Text'
}
]
});
});
it('properly decodes a Result<Result<(), u32>, Text>', (): void => {
expect(
getTypeDef('Result<Result<Null,u32>,Text>')
).toEqual({
info: TypeDefInfo.Result,
type: 'Result<Result<Null,u32>,Text>',
sub: [
{
info: TypeDefInfo.Result,
type: 'Result<Null,u32>',
sub: [
{
info: TypeDefInfo.Plain,
type: 'Null'
},
{
info: TypeDefInfo.Plain,
type: 'u32'
}
]
},
{
info: TypeDefInfo.Plain,
type: 'Text'
}
]
});
});
it('returns a type structure', (): void => {
expect(
getTypeDef('(u32, Compact<u32>, Vec<u64>, Option<u128>, DoubleMap<u128>, (Text,Vec<(Bool,u128)>))')
+14 -13
View File
@@ -56,7 +56,6 @@ function _decodeStruct (value: TypeDef, type: string, subType: string): TypeDef
return _decodeSet(value, parsed[keys[0]]);
}
value.info = TypeDefInfo.Struct;
value.sub = keys.map((name): TypeDef =>
// eslint-disable-next-line @typescript-eslint/no-use-before-define
getTypeDef(parsed[name], name)
@@ -74,7 +73,6 @@ function _decodeFixedVec (value: TypeDef, type: string, subType: string): TypeDe
// as a first round, only u8 via u8aFixed, we can add more support
assert(vecLen <= 256, `${type}: Only support for [Type; <length>], where length <= 256`);
value.info = TypeDefInfo.VecFixed;
value.ext = { length: vecLen, type: vecType } as unknown as TypeDefExtVecFixed;
return value;
@@ -82,7 +80,6 @@ function _decodeFixedVec (value: TypeDef, type: string, subType: string): TypeDe
// decode a tuple
function _decodeTuple (value: TypeDef, type: string, subType: string): TypeDef {
value.info = TypeDefInfo.Tuple;
value.sub = typeSplit(subType).map((inner): TypeDef =>
// eslint-disable-next-line @typescript-eslint/no-use-before-define
getTypeDef(inner)
@@ -91,7 +88,7 @@ function _decodeTuple (value: TypeDef, type: string, subType: string): TypeDef {
return value;
}
function hasWrapper (type: string, [start, end]: [string, string, any]): boolean {
function hasWrapper (type: string, [start, end]: [string, string, TypeDefInfo, any?]): boolean {
if (type.substr(0, start.length) !== start) {
return false;
}
@@ -101,21 +98,23 @@ function hasWrapper (type: string, [start, end]: [string, string, any]): boolean
return true;
}
const nestedExtraction: [string, string, (value: TypeDef, type: string, subType: string) => TypeDef][] = [
['[', ']', _decodeFixedVec],
['{', '}', _decodeStruct],
['(', ')', _decodeTuple]
const nestedExtraction: [string, string, TypeDefInfo, (value: TypeDef, type: string, subType: string) => TypeDef][] = [
['[', ']', TypeDefInfo.VecFixed, _decodeFixedVec],
['{', '}', TypeDefInfo.Struct, _decodeStruct],
['(', ')', TypeDefInfo.Tuple, _decodeTuple],
// the inner for result is the same as tuple
['Result<', '>', TypeDefInfo.Result, _decodeTuple]
];
const wrappedExtraction: [string, string, TypeDefInfo][] = [
['Compact<', '>', TypeDefInfo.Compact],
['Option<', '>', TypeDefInfo.Option],
['Vec<', '>', TypeDefInfo.Vec],
['DoubleMap<', '>', TypeDefInfo.DoubleMap],
['Linkage<', '>', TypeDefInfo.Linkage],
['DoubleMap<', '>', TypeDefInfo.DoubleMap]
['Option<', '>', TypeDefInfo.Option],
['Vec<', '>', TypeDefInfo.Vec]
];
function extractSubType (type: string, [start, end]: [string, string, any]): string {
function extractSubType (type: string, [start, end]: [string, string, TypeDefInfo, any?]): string {
return type.substr(start.length, type.length - start.length - end.length);
}
@@ -129,7 +128,9 @@ export function getTypeDef (_type: string, name?: string): TypeDef {
);
if (nested) {
return nested[2](value, type, extractSubType(type, nested));
value.info = nested[2];
return nested[3](value, type, extractSubType(type, nested));
}
const wrapped = wrappedExtraction.find((wrapped): boolean =>
+1 -1
View File
@@ -5,7 +5,7 @@
type Mapper = (value: string) => string;
const ALLOWED_BOXES = ['Compact', 'DoubleMap', 'Linkage', 'Option', 'Vec'];
const ALLOWED_BOXES = ['Compact', 'DoubleMap', 'Linkage', 'Result', 'Option', 'Vec'];
const mappings: Mapper[] = [
// alias <T::InherentOfflineReport as InherentOfflineReport>::Inherent -> InherentOfflineReport
+1
View File
@@ -18,6 +18,7 @@ export enum TypeDefInfo {
Linkage,
Option,
Plain,
Result,
Set,
Struct,
Tuple,
+1
View File
@@ -12,6 +12,7 @@ export { default as Compact } from './Compact';
// export { default as Date } from './Date';
export { default as Enum } from './Enum';
export { default as Option } from './Option';
export { default as Result } from './Result';
export { default as Set } from './Set';
export { default as Struct } from './Struct';
// export { default as StructAny } from './StructAny';
@@ -4,14 +4,8 @@
export default {
types: {
// Not 100% on this one this is defined as - result::Result<(), Error>
// we would probably need a result codec type (instead of approximating it)
DispatchResult: {
_enum: {
Ok: 'Null',
Error: 'Text'
}
},
// This is not deprecated, only there for a short while, replaced by Result<(), DispatchError>
DispatchResult: 'Result<(), Text>',
DispatchResultOf: 'DispatchResult'
}
};
@@ -1,17 +1,17 @@
// Auto-generated via `yarn build:interfaces`, do not edit
/* eslint-disable @typescript-eslint/no-empty-interface */
import { Enum } from '@polkadot/types/codec';
import { Text } from '@polkadot/types/primitive';
import { Result } from '@polkadot/types/codec';
import { Null, Text } from '@polkadot/types/primitive';
/** Enum */
export interface DispatchResult extends Enum {
/** 0:: Ok */
readonly isOk: boolean;
/** 1:: Error(Text) */
/** Result<Null, Text> */
export interface DispatchResult extends Result<Null, Text> {
/** Error:: (Text) */
readonly isError: boolean;
/** Text */
readonly asError: Text;
/** Ok:: */
readonly isOk: boolean;
}
/** DispatchResult */
@@ -9,13 +9,7 @@ import { isString, stringCamelCase, stringUpperFirst } from '@polkadot/util';
import { getTypeDef } from '../../codec/create';
import * as definitions from '../../interfaces/definitions';
import {
createImportCode, createImports,
exportInterface, exportType,
formatCompact, formatOption, formatTuple, formatVec,
FOOTER, HEADER,
setImports, TypeImports
} from '../util';
import { createImportCode, createImports, exportInterface, exportType, formatCompact, formatOption, formatResult, formatTuple, formatVec, FOOTER, HEADER, setImports, TypeImports } from '../util';
interface Imports extends TypeImports {
interfaces: [string, string][];
@@ -91,6 +85,34 @@ function tsPlain ({ name: plainName, type }: TypeDef, imports: TypeImports): str
return exportType(plainName, type);
}
function tsResultGetter (resultName = '', getter: 'Ok' | 'Error', { info, name = '', type }: TypeDef, imports: TypeImports): string {
const [resultType, asGetter] = type === 'Null'
? ['', '']
: [`(${type})`, createGetter(`as${getter}`, type, imports)];
const isGetter = createGetter(`is${getter}`, 'boolean', imports, `${getter}:: ${name}${resultType}`);
switch (info) {
case TypeDefInfo.Plain:
case TypeDefInfo.Vec:
return `${isGetter}${asGetter}`;
default:
throw new Error(`Result: ${resultName}: Unhandled type ${TypeDefInfo[info]}`);
}
}
function tsResult ({ name: resultName, sub, type }: TypeDef, imports: TypeImports): string {
const [okDef, errorDef] = (sub as TypeDef[]);
const inner = [
tsResultGetter(resultName, 'Error', errorDef, imports),
tsResultGetter(resultName, 'Ok', okDef, imports)
].join('');
setImports(imports, [type]);
return exportInterface(resultName, formatResult(okDef.type, errorDef.type), inner);
}
function _tsStructGetterType (structName: string | undefined, { info, sub, type }: TypeDef, imports: TypeImports): [string, string] {
let _type;
@@ -203,6 +225,7 @@ function generateInterfaces ({ types }: { types: Record<string, any> }, imports:
[TypeDefInfo.Null]: errorUnhandled,
[TypeDefInfo.Option]: tsOption,
[TypeDefInfo.Plain]: tsPlain,
[TypeDefInfo.Result]: tsResult,
[TypeDefInfo.Set]: tsSet,
[TypeDefInfo.Struct]: tsStruct,
[TypeDefInfo.Tuple]: tsTuple,
@@ -40,6 +40,13 @@ export function formatCompact (inner: string): string {
return `Compact<${inner}>`;
}
/**
* Given the inner `O` & `E`, return a `Result<O, E>` string
*/
export function formatResult (innerOk: string, innerError: string): string {
return `Result<${innerOk}, ${innerError}>`;
}
/**
* Given the inner `T`, return a `Option<T>` string
*/