Multiple Codec property assignments helper (#4182)

* Multiple Codec property assignments helper

* Pass index

* consistency in naming

* typesToMap external

* Loop on index

* any -> unknown

* CHANGELOG

* Adjust

* Fix Enum with Array keys
This commit is contained in:
Jaco
2021-11-07 03:00:37 +02:00
committed by GitHub
parent 61a7fe6307
commit 32b9a39b1e
9 changed files with 80 additions and 64 deletions
+9 -4
View File
@@ -114,9 +114,14 @@ describe('Enum', (): void => {
});
it('has correct isXyz/asXyz (Enum.with)', (): void => {
const test = new (Enum.with({ First: Text, Second: U32, Third: U32 }))(registry, { Second: 42 }) as any as { isSecond: boolean; asSecond: U32; asThird: never };
const test = new (Enum.with({ First: Text, Second: U32, Third: U32 }))(registry, { Second: 42 }) as any as { isFirst: boolean; isSecond: boolean; asSecond: U32; isThird: boolean; asThird: never };
const asKeys = Object.keys(test).filter((k) => k.startsWith('as'));
const isKeys = Object.keys(test).filter((k) => k.startsWith('is'));
expect(asKeys).toEqual(['asFirst', 'asSecond', 'asThird']);
expect(isKeys).toEqual(['isFirst', 'isSecond', 'isThird']);
expect([test.isFirst, test.isSecond, test.isThird]).toEqual([false, true, false]);
expect(test.isSecond).toEqual(true);
expect(test.asSecond.toNumber()).toEqual(42);
expect((): never => test.asThird).toThrow(/Cannot convert 'Second' via asThird/);
});
@@ -250,9 +255,9 @@ describe('Enum', (): void => {
});
it('has correct isXyz getters (Enum.with)', (): void => {
const test = new (Enum.with(['First', 'Second', 'Third']))(registry, 'Second') as any as { isSecond: boolean; asSecond: never };
const test = new (Enum.with(['First', 'Second', 'Third']))(registry, 'Second') as any as { isFirst: boolean; isSecond: boolean; isThird: boolean };
expect(test.isSecond).toEqual(true);
expect([test.isFirst, test.isSecond, test.isThird]).toEqual([false, true, false]);
});
describe('utils', (): void => {
+24 -21
View File
@@ -8,8 +8,7 @@ import type { AnyJson, Codec, Constructor, IEnum, Registry } from '../types';
import { assert, hexToU8a, isHex, isNumber, isObject, isString, isU8a, isUndefined, stringCamelCase, stringify, stringUpperFirst, u8aConcat, u8aToHex } from '@polkadot/util';
import { Null } from '../primitive/Null';
import { Struct } from './Struct';
import { defineProperty, mapToTypeMap } from './utils';
import { defineProperties, mapToTypeMap, typesToMap } from './utils';
// export interface, this is used in Enum.with, so required as public by TS
export interface EnumConstructor<T = Codec> {
@@ -86,7 +85,7 @@ function extractDef (registry: Registry, _def: Record<string, string | Construct
};
}
function createFromValue (registry: Registry, def: TypesDef, index = 0, value?: any): Decoded {
function createFromValue (registry: Registry, def: TypesDef, index = 0, value?: unknown): Decoded {
const entry = Object.values(def).find((e) => e.index === index);
assert(!isUndefined(entry), () => `Unable to create Enum via index ${index}, in ${Object.keys(def).join(', ')}`);
@@ -99,7 +98,7 @@ function createFromValue (registry: Registry, def: TypesDef, index = 0, value?:
};
}
function decodeFromJSON (registry: Registry, def: TypesDef, key: string, value?: any): Decoded {
function decodeFromJSON (registry: Registry, def: TypesDef, key: string, value?: unknown): Decoded {
// JSON comes in the form of { "<type (camelCase)>": "<value for type>" }, here we
// additionally force to lower to ensure forward compat
const keys = Object.keys(def).map((k) => k.toLowerCase());
@@ -122,7 +121,7 @@ function decodeFromString (registry: Registry, def: TypesDef, value: string): De
: decodeFromJSON(registry, def, value);
}
function decodeFromValue (registry: Registry, def: TypesDef, value?: any): Decoded {
function decodeFromValue (registry: Registry, def: TypesDef, value?: unknown): Decoded {
if (isU8a(value)) {
// nested, we don't want to match isObject below
if (value.length) {
@@ -142,7 +141,7 @@ function decodeFromValue (registry: Registry, def: TypesDef, value?: any): Decod
return createFromValue(registry, def, Object.values(def)[0].index);
}
function decodeEnum (registry: Registry, def: TypesDef, value?: any, index?: number): Decoded {
function decodeEnum (registry: Registry, def: TypesDef, value?: unknown, index?: number): Decoded {
// 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)) {
@@ -198,25 +197,29 @@ export class Enum implements IEnum {
}
public static with (Types: Record<string, string | Constructor> | Record<string, number> | string[]): EnumConstructor<Enum> {
const keys = Array.isArray(Types)
? Types
: Object.keys(Types);
const asKeys = new Array<string>(keys.length);
const isKeys = new Array<string>(keys.length);
for (let i = 0; i < keys.length; i++) {
const name = stringUpperFirst(stringCamelCase(keys[i].replace(' ', '_')));
asKeys[i] = `as${name}`;
isKeys[i] = `is${name}`;
}
return class extends Enum {
constructor (registry: Registry, value?: unknown, index?: number) {
super(registry, Types, value, index);
const keys = Object.keys(this.#def);
defineProperties(this, isKeys, (_, i) => this.type === keys[i]);
defineProperties(this, asKeys, (k, i): Codec => {
assert(this[isKeys[i] as keyof this], () => `Cannot convert '${this.type}' via ${k}`);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const name = stringUpperFirst(stringCamelCase(key.replace(' ', '_')));
const askey = `as${name}`;
const iskey = `is${name}`;
defineProperty(this, iskey, () => this.type === key);
defineProperty(this, askey, (): Codec => {
assert(this[iskey as keyof this], () => `Cannot convert '${this.type}' via ${askey}`);
return this.value;
});
}
return this.value;
});
}
};
}
@@ -376,7 +379,7 @@ export class Enum implements IEnum {
return out;
}, {});
return Struct.typesToMap(this.registry, typeMap);
return typesToMap(this.registry, typeMap);
}
/**
+2 -6
View File
@@ -7,7 +7,7 @@ import type { AnyJson, Codec, Registry } from '../types';
import { isFunction, stringify } from '@polkadot/util';
import { compareMap, defineProperty } from './utils';
import { compareMap, defineProperties } from './utils';
/** @internal */
function decodeJson (value?: Record<string, unknown> | null): [string, any][] {
@@ -34,11 +34,7 @@ export class Json extends Map<string, any> implements Codec {
this.registry = registry;
for (let i = 0; i < decoded.length; i++) {
const [key] = decoded[i];
defineProperty(this, key, () => this.get(key) as Codec);
}
defineProperties(this, decoded.map(([k]) => k), (k) => this.get(k));
}
/**
+9 -8
View File
@@ -7,7 +7,7 @@ import type { Constructor, ISet, Registry } from '../types';
import { assert, BN, bnToBn, bnToU8a, isBn, isNumber, isString, isU8a, isUndefined, stringCamelCase, stringify, stringUpperFirst, u8aToBn, u8aToHex, u8aToU8a } from '@polkadot/util';
import { compareArray, defineProperty } from './utils';
import { compareArray, defineProperties } from './utils';
type SetValues = Record<string, number | BN>;
@@ -104,17 +104,18 @@ export class CodecSet extends Set<string> implements ISet<string> {
}
public static with (values: SetValues, bitLength?: number): Constructor<CodecSet> {
const keys = Object.keys(values);
const isKeys = new Array<string>(keys.length);
for (let i = 0; i < keys.length; i++) {
isKeys[i] = `is${stringUpperFirst(stringCamelCase(keys[i]))}`;
}
return class extends CodecSet {
constructor (registry: Registry, value?: unknown) {
super(registry, values, value as undefined, bitLength);
const keys = Object.keys(values);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
defineProperty(this, `is${stringUpperFirst(stringCamelCase(key))}`, () => this.strings.includes(key));
}
defineProperties(this, isKeys, (_, i) => this.strings.includes(keys[i]));
}
};
}
+5 -22
View File
@@ -7,7 +7,7 @@ import type { AnyJson, BareOpts, Codec, Constructor, ConstructorDef, IStruct, Re
import { assert, hexToU8a, isBoolean, isFunction, isHex, isObject, isU8a, isUndefined, stringCamelCase, stringify, u8aConcat, u8aToHex } from '@polkadot/util';
import { compareMap, decodeU8a, defineProperty, mapToTypeMap } from './utils';
import { compareMap, decodeU8a, defineProperties, mapToTypeMap, typesToMap } from './utils';
type TypesDef<T = Codec> = Record<string, string | Constructor<T>>;
@@ -147,34 +147,17 @@ export class Struct<
}
public static with<S extends TypesDef> (Types: S, jsonMap?: Map<string, string>): Constructor<Struct<S>> {
const keys = Object.keys(Types);
return class extends Struct<S> {
constructor (registry: Registry, value?: unknown) {
super(registry, Types, value as HexString, jsonMap);
const keys = Object.keys(Types);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
defineProperty(this, key, () => this.get(key as keyof S));
}
defineProperties(this, keys, (k) => this.get(k));
}
};
}
public static typesToMap (registry: Registry, Types: Record<string, Constructor>): Record<string, string> {
const entries = Object.entries(Types);
const result: Record<string, string> = {};
for (let i = 0; i < entries.length; i++) {
const [key, Type] = entries[i];
result[key] = registry.getClassName(Type) || new Type(registry).toRawType();
}
return result;
}
/**
* @description The available keys for this struct
*/
@@ -299,7 +282,7 @@ export class Struct<
* @description Returns the base runtime type name for this instance
*/
public toRawType (): string {
return stringify(Struct.typesToMap(this.registry, this.#Types));
return stringify(typesToMap(this.registry, this.#Types));
}
/**
@@ -3,7 +3,7 @@
import { isUndefined } from '@polkadot/util';
const SKIP_KEYS = ['hash'];
const SKIP_KEYS = ['entries', 'hash', 'keys', 'values'];
export function defineProperty (that: object, key: string, get: () => unknown): void {
if (SKIP_KEYS.includes(key)) {
@@ -16,3 +16,12 @@ export function defineProperty (that: object, key: string, get: () => unknown):
Object.defineProperty(that, key, { enumerable: true, get });
}
}
export function defineProperties (that: object, keys: string[], get: (k: string, i: number) => unknown): void {
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const index = i;
defineProperty(that, key, () => get(key, index));
}
}
+2 -1
View File
@@ -5,7 +5,8 @@ export { compareArray } from './compareArray';
export { compareMap } from './compareMap';
export { compareSet } from './compareSet';
export { decodeU8a, decodeU8aVec } from './decodeU8a';
export { defineProperty } from './defineProperty';
export { defineProperties, defineProperty } from './defineProperty';
export { mapToTypeMap } from './mapToTypeMap';
export { typeToConstructor } from './typeToConstructor';
export { sortAsc, sortSet, sortMap } from './sortValues';
export { typesToMap } from './typesToMap';
@@ -0,0 +1,17 @@
// Copyright 2017-2021 @polkadot/types authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Constructor, Registry } from '../../types';
export function typesToMap (registry: Registry, Types: Record<string, Constructor>): Record<string, string> {
const entries = Object.entries(Types);
const result: Record<string, string> = {};
for (let i = 0; i < entries.length; i++) {
const [key, Type] = entries[i];
result[key] = registry.getClassName(Type) || new Type(registry).toRawType();
}
return result;
}