diff --git a/CHANGELOG.md b/CHANGELOG.md index d80e5bdfae..e09e0d397e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ Changes: - Convert metadata, rpc and api to use lazy decoration - Split u8a decoding paths for Vec (different from general path) - Keep track of initial u8a decoded length inside Codec -- Move PortableRegistry location inside `@polkadot/types/metadata` +- Extract multi property assignments for `Struct`, `Set`, `Json` & `Enum` +- Move `PortableRegistry` location inside `@polkadot/types/metadata` - Bump static metadata for latest Substrate, Polkadot & Kusama diff --git a/packages/types/src/codec/Enum.spec.ts b/packages/types/src/codec/Enum.spec.ts index b96df209b9..91900a0057 100644 --- a/packages/types/src/codec/Enum.spec.ts +++ b/packages/types/src/codec/Enum.spec.ts @@ -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 => { diff --git a/packages/types/src/codec/Enum.ts b/packages/types/src/codec/Enum.ts index 3ded1661d5..28599de93b 100644 --- a/packages/types/src/codec/Enum.ts +++ b/packages/types/src/codec/Enum.ts @@ -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 { @@ -86,7 +85,7 @@ function extractDef (registry: Registry, _def: Record 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 { "": "" }, 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 | Record | string[]): EnumConstructor { + const keys = Array.isArray(Types) + ? Types + : Object.keys(Types); + const asKeys = new Array(keys.length); + const isKeys = new Array(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); } /** diff --git a/packages/types/src/codec/Json.ts b/packages/types/src/codec/Json.ts index a13d147707..6b084eedea 100644 --- a/packages/types/src/codec/Json.ts +++ b/packages/types/src/codec/Json.ts @@ -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 | null): [string, any][] { @@ -34,11 +34,7 @@ export class Json extends Map 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)); } /** diff --git a/packages/types/src/codec/Set.ts b/packages/types/src/codec/Set.ts index d6ee093254..871e2b86da 100644 --- a/packages/types/src/codec/Set.ts +++ b/packages/types/src/codec/Set.ts @@ -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; @@ -104,17 +104,18 @@ export class CodecSet extends Set implements ISet { } public static with (values: SetValues, bitLength?: number): Constructor { + const keys = Object.keys(values); + const isKeys = new Array(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])); } }; } diff --git a/packages/types/src/codec/Struct.ts b/packages/types/src/codec/Struct.ts index b4f48a3509..af0b0eb4f6 100644 --- a/packages/types/src/codec/Struct.ts +++ b/packages/types/src/codec/Struct.ts @@ -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 = Record>; @@ -147,34 +147,17 @@ export class Struct< } public static with (Types: S, jsonMap?: Map): Constructor> { + const keys = Object.keys(Types); + return class extends Struct { 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): Record { - const entries = Object.entries(Types); - const result: Record = {}; - - 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)); } /** diff --git a/packages/types/src/codec/utils/defineProperty.ts b/packages/types/src/codec/utils/defineProperty.ts index e5e7df0d4a..aeed071827 100644 --- a/packages/types/src/codec/utils/defineProperty.ts +++ b/packages/types/src/codec/utils/defineProperty.ts @@ -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)); + } +} diff --git a/packages/types/src/codec/utils/index.ts b/packages/types/src/codec/utils/index.ts index 97ad9d5740..d7e117c232 100644 --- a/packages/types/src/codec/utils/index.ts +++ b/packages/types/src/codec/utils/index.ts @@ -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'; diff --git a/packages/types/src/codec/utils/typesToMap.ts b/packages/types/src/codec/utils/typesToMap.ts new file mode 100644 index 0000000000..bd6e05b672 --- /dev/null +++ b/packages/types/src/codec/utils/typesToMap.ts @@ -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): Record { + const entries = Object.entries(Types); + const result: Record = {}; + + for (let i = 0; i < entries.length; i++) { + const [key, Type] = entries[i]; + + result[key] = registry.getClassName(Type) || new Type(registry).toRawType(); + } + + return result; +}