From e49cd38ace8eaca2819bd5e6cd62f9820463cb69 Mon Sep 17 00:00:00 2001 From: Fuxing Loh Date: Fri, 16 Apr 2021 13:48:28 +0800 Subject: [PATCH 1/3] added path based PrecisionMapping --- .gitignore | 2 + .../jellyfish-json/__tests__/index.test.ts | 2 + .../jellyfish-json/__tests__/remap.test.ts | 308 +++++++----------- packages/jellyfish-json/src/index.ts | 3 +- packages/jellyfish-json/src/remap.ts | 189 ++++------- 5 files changed, 190 insertions(+), 314 deletions(-) diff --git a/.gitignore b/.gitignore index c7cdfb731a..e273786d3c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ node_modules # dist & pack dist *.tgz + +coverage diff --git a/packages/jellyfish-json/__tests__/index.test.ts b/packages/jellyfish-json/__tests__/index.test.ts index 03259bcd21..e1526f773f 100644 --- a/packages/jellyfish-json/__tests__/index.test.ts +++ b/packages/jellyfish-json/__tests__/index.test.ts @@ -64,3 +64,5 @@ describe('stringify', () => { expect(string).toBe('{"bignumber":1200000000.00000001}') }) }) + +// TODO(fuxingloh): mapping test diff --git a/packages/jellyfish-json/__tests__/remap.test.ts b/packages/jellyfish-json/__tests__/remap.test.ts index acc08d4496..d26be7b631 100644 --- a/packages/jellyfish-json/__tests__/remap.test.ts +++ b/packages/jellyfish-json/__tests__/remap.test.ts @@ -1,219 +1,161 @@ -import { BigNumber, JellyfishJSON, LosslessNumber } from '../src' - -it('remap deeply', () => { - const { result: jsonObj } = JellyfishJSON.parse(`{"result": { - "bestblock": "7048e23a5cb86cc7751ef63a87a0ca6e0a00a786bce8af88ae3d1292c5414954", - "confirmations": 1, - "value": 1200000000.00000001, - "scriptPubKey": { - "asm": "OP_DUP OP_HASH160 b36814fd26190b321aa985809293a41273cfe15e OP_EQUALVERIFY OP_CHECKSIG", - "hex": "76a914b36814fd26190b321aa985809293a41273cfe15e88ac", - "reqSigs": 1, - "type": "pubkeyhash", - "addresses": [ "mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU"], - "customX": { - "nestedX": 1200000000.00000001 - }, - "customY": { - "nestedY": 1200000000.00000001 - } - }, - "coinbase": true, - "custom": 99 - }}`, { - scriptPubKey: { - reqSigs: 'number', - customX: { - nestedX: 'bignumber' +import { parse } from 'lossless-json' +import { PrecisionMapping, remap } from '../src/remap' +import { BigNumber } from '../src' + +it('should remap everything in the path', () => { + const losslessObj = parse(` + { + "a": 1.2, + "b": [1.2, 1.2], + "c": { "c1": 1.2, "c2": [1.2] }, + "d": { + "e": { + "e1": 1.2, + "e2": [1.2, 1.2], + "e3": { "e3a": 1.2, "e3b": [1.2] } + }, + "f": { + "g": 1.2, + "f1": 1.2 } }, - value: 'bignumber', - confirmations: 'lossless' + "h": 1.2 + } + `) + const remapped = remap(losslessObj, { + a: 'bignumber', + b: 'bignumber', + c: 'bignumber', + d: { + e: 'bignumber', + f: { + g: 'bignumber' + } + } }) - expect(jsonObj.value instanceof BigNumber).toBe(true) - expect(jsonObj.value.toString()).toBe('1200000000.00000001') + expect(remapped.a instanceof BigNumber).toBe(true) - expect(jsonObj.confirmations instanceof LosslessNumber).toBe(true) - expect(jsonObj.confirmations.toString()).toBe('1') + expect(remapped.b[0] instanceof BigNumber).toBe(true) + expect(remapped.b[1] instanceof BigNumber).toBe(true) - expect(jsonObj.scriptPubKey.reqSigs).toBe(1) - expect(jsonObj.scriptPubKey.customX.nestedX instanceof BigNumber).toBe(true) - expect(jsonObj.scriptPubKey.customX.nestedX.toString()).toBe('1200000000.00000001') - expect(jsonObj.scriptPubKey.customY.nestedY).toBe(1200000000) + expect(remapped.c.c1 instanceof BigNumber).toBe(true) + expect(remapped.c.c2[0] instanceof BigNumber).toBe(true) - expect(jsonObj.custom).toBe(99) -}) + expect(remapped.d.e.e1 instanceof BigNumber).toBe(true) + expect(remapped.d.e.e2[0] instanceof BigNumber).toBe(true) + expect(remapped.d.e.e2[1] instanceof BigNumber).toBe(true) -it('should remap array in deeply nested array', async () => { - const { result: jsonObj } = JellyfishJSON.parse(`{ - "result": { - "value": 38, - "customX": [ - {"nestedA": { "a1": 1, "a2": 2, "a3": 3}}, - {"nestedB": { "b1": 4, "b2": 5, "b3": 6}}, - {"nestedC": 99} - ], - "customY": [50, 55, 60], - "customZ": [70, 75, 80] - }, "error": {}, "id": 3 - }`, { - value: 'bignumber', - customX: { nestedA: { a2: 'bignumber' } }, - customY: 'bignumber' - }) + expect(remapped.d.e.e3.e3a instanceof BigNumber).toBe(true) + expect(remapped.d.e.e3.e3b[0] instanceof BigNumber).toBe(true) - expect(jsonObj.value instanceof BigNumber).toBe(true) - expect(jsonObj.value.toString()).toBe('38') - expect(jsonObj.customX[0].nestedA.a2 instanceof BigNumber).toBe(true) - expect(jsonObj.customX[0].nestedA.a2.toString()).toBe('2') - expect(jsonObj.customY.every((y: BigNumber) => y instanceof BigNumber)).toBe(true) - expect(jsonObj.customZ.every((z: number) => typeof z === 'number')).toBe(true) -}) + expect(remapped.d.f.g instanceof BigNumber).toBe(true) -it('should remap array at object root', async () => { - const parsed = JellyfishJSON.parse(`[ - {"group1": {"subgroup1": 4000}}, - {"group2": {"subgroup2": 5000}}, - {"group3": {"subgroup3": 6000}} - ]`, { - group2: { subgroup2: 'bignumber' }, - group3: { subgroup3: 'lossless' } - }) + expect(remapped.d.f.f1).toBe(1.2) - expect(parsed[0].group1.subgroup1).toBe(4000) - expect(parsed[1].group2.subgroup2 instanceof BigNumber).toBe(true) - expect(parsed[1].group2.subgroup2.toString()).toBe('5000') - expect(parsed[2].group3.subgroup3 instanceof LosslessNumber).toBe(true) - expect(parsed[2].group3.subgroup3.toString()).toBe('6000') + expect(remapped.h).toBe(1.2) }) -it('should remap object at root with key mapping', () => { - const parsed = JellyfishJSON.parse(`{ - "big": 123.4, - "num": 1000 - }`, { - big: 'bignumber' - }) - expect(parsed.big instanceof BigNumber).toBe(true) - expect(parsed.num).toBe(1000) -}) +function parseAndRemap (text: string, precision: PrecisionMapping): any { + const losslessObj = parse(text) + return remap(losslessObj, precision) +} -it('should remap object deeply with key mapping', () => { - const parsed = JellyfishJSON.parse(`{ - "deeply": { - "big": 123, +describe('remap individually', () => { + it('should remap object at root with precision', () => { + const parsed = parseAndRemap(`{ + "big": 123.4, "num": 1000 - } - }`, { - deeply: { + }`, { big: 'bignumber' - } - }) - expect(parsed.deeply.big instanceof BigNumber).toBe(true) - expect(parsed.deeply.num).toBe(1000) -}) - -it('should remap all array with same precision', () => { - const parsed = JellyfishJSON.parse(`[ - 100, - 200 - ]`, 'bignumber') - - expect(parsed[0] instanceof BigNumber).toBe(true) - expect(parsed[1] instanceof BigNumber).toBe(true) -}) - -it('should remap all array object with same precision', () => { - const parsed = JellyfishJSON.parse(`[ - { - "big": 100 - }, - { - "big": 200.2 - } - ]`, { - big: 'bignumber' + }) + expect(parsed.big instanceof BigNumber).toBe(true) + expect(parsed.num).toBe(1000) }) - expect(parsed[0].big instanceof BigNumber).toBe(true) - expect(parsed[1].big instanceof BigNumber).toBe(true) -}) - -it('should remap all array object with same precision deeply', () => { - const parsed = JellyfishJSON.parse(`[ - { + it('should remap object deeply with precision', () => { + const parsed = parseAndRemap(`{ "deeply": { - "big": 123 + "big": 123, + "num": 1000 } - }, - { - "deeply": { - "big": 123.4 + }`, { + deeply: { + big: 'bignumber' } - } - ]`, { - deeply: { - big: 'bignumber' - } + }) + expect(parsed.deeply.big instanceof BigNumber).toBe(true) + expect(parsed.deeply.num).toBe(1000) }) - expect(parsed[0].deeply.big instanceof BigNumber).toBe(true) - expect(parsed[1].deeply.big instanceof BigNumber).toBe(true) -}) - -it('should remap linear array', async () => { - const parsed = JellyfishJSON.parse(` - { - "items": [1,2,3] + it('should remap array value with precision', () => { + const remapped = parseAndRemap(`{ + "array": [ + 100, + 200 + ] }`, { - items: 'bignumber' - }) + array: 'bignumber' + }) - expect(parsed.items.every((i: BigNumber) => i instanceof BigNumber)).toBe(true) - expect(parsed.items[0].toString()).toBe('1') - expect(parsed.items[1].toString()).toBe('2') - expect(parsed.items[2].toString()).toBe('3') -}) + expect(remapped.array[0] instanceof BigNumber).toBe(true) + expect(remapped.array[1] instanceof BigNumber).toBe(true) + }) -it('should throw error if unmatched precision mapping with text', async () => { - const t: any = () => { - return JellyfishJSON.parse('{"result":{"nested": 1, "nestedB": 1}}', { - nested: { - something: 'bignumber' + it('should remap array object with precision', () => { + const parsed = parseAndRemap(`[ + { + "big": 100 + }, + { + "big": 200.2 } + ]`, { + big: 'bignumber' }) - } - expect(t).toThrow('JellyfishJSON.parse nested: 1 with [object Object] precision is not supported') -}) + expect(parsed[0].big instanceof BigNumber).toBe(true) + expect(parsed[1].big instanceof BigNumber).toBe(true) + }) -it('should throw error if invalid type is converted', async () => { - const t: any = () => { - return JellyfishJSON.parse('{"result":{"value": {}}}', { - value: 'bignumber' + it('should remap array object with precision deeply', () => { + const parsed = parseAndRemap(`[ + { + "deeply": { + "big": 123, + "num": 1 + } + }, + { + "deeply": { + "big": 123.4, + "num": 1 + } + } + ]`, { + deeply: { + big: 'bignumber' + } }) - } - expect(t).toThrow('JellyfishJSON.parse value: [object Object] with bignumber precision is not supported') -}) - -it('missing property should be ignored', () => { - const text = '{"result":{"nested":1}}' - const object = JellyfishJSON.parse(text, { - nested: 'number', - invalid: 'lossless' + expect(parsed[0].deeply.big instanceof BigNumber).toBe(true) + expect(parsed[0].deeply.num).toBe(1) + expect(parsed[1].deeply.big instanceof BigNumber).toBe(true) + expect(parsed[1].deeply.num).toBe(1) }) - - expect(JSON.stringify(object)).toBe(text) }) -it('strict precision testing: should the specific struct value is parsed', async () => { - const parsed = JellyfishJSON.parse(` - {"foo": {"bar": 123}, "bar": 456 } - `, { foo: { bar: 'number' }, bar: 'bignumber' }) - - expect(parsed.foo.bar).toBe(123) - expect(parsed.bar instanceof BigNumber).toBe(true) - expect(parsed.bar.toString()).toBe('456') +describe('remap invalid mapping should succeed', () => { + it('should ignore invalid remapping', () => { + const parsed = parseAndRemap(`{ + "ignored": 123.4, + "num": 1000 + }`, { + ignored: { + a: 'bignumber' + } + }) + expect(parsed.ignored).toBe(123.4) + expect(parsed.num).toBe(1000) + }) }) diff --git a/packages/jellyfish-json/src/index.ts b/packages/jellyfish-json/src/index.ts index b5c24e0c30..42ecfe021a 100644 --- a/packages/jellyfish-json/src/index.ts +++ b/packages/jellyfish-json/src/index.ts @@ -62,7 +62,8 @@ export const JellyfishJSON = { } } - return remap(text, precision) + const losslessObj = parse(text) + return remap(losslessObj, precision) }, /** diff --git a/packages/jellyfish-json/src/remap.ts b/packages/jellyfish-json/src/remap.ts index 134739fda0..12a80e6b22 100644 --- a/packages/jellyfish-json/src/remap.ts +++ b/packages/jellyfish-json/src/remap.ts @@ -1,164 +1,93 @@ import BigNumber from 'bignumber.js' -import { LosslessNumber, parse } from 'lossless-json' -import { Precision } from './index' +import { LosslessNumber } from 'lossless-json' /** - * Manual key based mapping of each precision value + * Path based precision mapping + * Specifying 'bignumber' will automatically map all Number in that path as 'bignumber'. + * Otherwise it will default to number. This works deeply. + * + * @example + * path = { + * a: 'bignumber', + * b: { + * c: 'bignumber' + * } + * } + * value = { + * a: BigNumber, + * b: { + * c: { + * d: BigNumber, + * e: [BigNumber, BigNumber] + * }, + * f: number + * }, + * g: number + * } */ export interface PrecisionMapping { - [key: string]: Precision | PrecisionMapping + [path: string]: 'bignumber' | PrecisionMapping } -enum MappingAction { - NONE = 0, - PRECISION = 1, - PRECISION_LOOP = 2, - DEEPLY_PRECISION_MAPPING = 3, - DEFAULT_NUMBER = 4, - DEEPLY_UNKNOWN = 5 +/** + * @param {any} losslessObj to deeply remap into bignumber or number. + * @param {'bignumber' | PrecisionMapping} precision path mapping + */ +export function remap (losslessObj: any, precision: PrecisionMapping): any { + return deepRemap(losslessObj, precision) } /** - * Remap lossless object with PrecisionMapping for each keys. - * - * @param text json string that is used to transform to a json object - * @param precision PrecisionMapping is a key value pair to allow revive value type + * @param {any} losslessObj to deeply remap + * @param {'bignumber' | PrecisionMapping} precision path mapping */ -export function remap (text: string, precision: PrecisionMapping): any { - const losslessObj = parse(text) - +function deepRemap (losslessObj: any, precision: 'bignumber' | PrecisionMapping): any { + if (typeof precision !== 'object') { + return reviveObjectAs(losslessObj, precision) + } if (Array.isArray(losslessObj)) { - return losslessObj.map((v: any) => { - const errMsg = validate(v, precision) - if (errMsg !== undefined) throw new Error(errMsg) - return remapLosslessObj(v, precision) - }) + return losslessObj.map(obj => deepRemap(obj, precision)) } - const errMsg = validate(losslessObj, precision) - if (errMsg !== undefined) throw new Error(errMsg) - return remapLosslessObj(losslessObj, precision) -} - -function validate (losslessObj: any, precision: PrecisionMapping): string | undefined { - for (const k in losslessObj) { - const type = precision[k] - const value = losslessObj[k] - - // throw err if invalid type conversion found - if (!isValid(value, type as Precision)) { - return `JellyfishJSON.parse ${k}: ${value as string} with ${type as string} precision is not supported` - } else if (isNestedObject(value)) { - return validate(value, precision) - } + for (const [key, value] of Object.entries(losslessObj)) { + losslessObj[key] = deepRemap(value, precision[key]) } -} - -function isNestedObject (value: any): boolean { - return typeof value === 'object' && !(value instanceof LosslessNumber) + return reviveObjectAs(losslessObj) } /** - * Validate the losslessObj type with precisionType + * Array will deeply remapped, object keys will be iterated on. * - * @param value losslessObj value - * @param precisionType Precision + * @param {any} losslessObj to revive + * @param {'bignumber'} precision to use, specific 'bignumber' for BigNumber else always default to number */ -function isValid (value: any, precisionType: Precision): boolean { - if (value instanceof LosslessNumber && typeof precisionType === 'object') { - // validation #1: unmatched precision parse - // eg: [LosslessObj] {nested: 1} parsed by [PrecisionMapping] {nested: { something: 'bignumber'}} - return false +function reviveObjectAs (losslessObj: any, precision?: 'bignumber' | string): any { + if (Array.isArray(losslessObj)) { + return losslessObj.map((v: any) => reviveObjectAs(v, precision)) } - if (typeof value === 'object' && Object.keys(value).length === 0) { - // validation #2: parsing invalid type - // eg: parsing empty object to bignumber - return false + if (losslessObj instanceof LosslessNumber) { + return reviveLosslessAs(losslessObj, precision) } - return true -} - -/** - * @param losslessObj lossless json object - * @param precision Precision - 'bignumber', 'number', 'lossless' - */ -function remapLosslessObj (losslessObj: any, precision: PrecisionMapping): any { - for (const k in losslessObj) { - const type: Precision | PrecisionMapping = precision[k] - - switch (getAction(losslessObj[k], type)) { - case MappingAction.PRECISION: - losslessObj[k] = mapValue(losslessObj[k], type as Precision) - break - - case MappingAction.PRECISION_LOOP: - losslessObj[k] = (losslessObj[k] as any[]).map(v => mapValue(v, type as Precision)) - break - - case MappingAction.DEFAULT_NUMBER: - losslessObj[k] = mapValue(losslessObj[k], 'number') - break - - case MappingAction.DEEPLY_PRECISION_MAPPING: - remapLosslessObj(losslessObj[k], type as PrecisionMapping) - break - - case MappingAction.DEEPLY_UNKNOWN: - remapLosslessObj(losslessObj[k], precision) - break + if (typeof losslessObj === 'object') { + for (const [key, value] of Object.entries(losslessObj)) { + losslessObj[key] = reviveObjectAs(value, precision) } + return losslessObj } return losslessObj } -function getAction (value: any, type: Precision | PrecisionMapping): MappingAction { - // typed with precision - if (typeof type === 'string' && value instanceof LosslessNumber) { - return MappingAction.PRECISION - } - - if (typeof type === 'string' && Array.isArray(value)) { - return MappingAction.PRECISION_LOOP - } - - // deeply with mapping - if (typeof type === 'object') { - return MappingAction.DEEPLY_PRECISION_MAPPING - } - - // number as default - if (value instanceof LosslessNumber) { - return MappingAction.DEFAULT_NUMBER - } - - // deeply with unknown - if (typeof value === 'object') { - return MappingAction.DEEPLY_UNKNOWN - } - - return MappingAction.NONE -} - /** - * @param precision Precision - * @param value is used to be converted a preferred type - * @returns converted value, 'lossless', 'bignumber', 'number' + * @param {LosslessNumber} losslessNum to revive as bignumber or number if precision != bignumber + * @param {'bignumber'} precision to use, specific 'bignumber' for BigNumber else always default to number */ -function mapValue (value: LosslessNumber, precision: Precision): LosslessNumber | BigNumber | Number { - switch (precision) { - case 'lossless': - return value - - case 'bignumber': - return new BigNumber(value.toString()) - - case 'number': - return Number(value.toString()) - - default: - throw new Error(`JellyfishJSON.parse ${precision as string} precision is not supported`) +function reviveLosslessAs (losslessNum: LosslessNumber, precision?: 'bignumber' | string): BigNumber | number { + if (precision === 'bignumber') { + return new BigNumber(losslessNum.toString()) } + + return Number(losslessNum.toString()) } From e5697a9f9fc9a4964d3c5b93ae18c50e02520d2b Mon Sep 17 00:00:00 2001 From: Fuxing Loh Date: Fri, 16 Apr 2021 13:56:33 +0800 Subject: [PATCH 2/3] apply & refactor jellyfish-json consumers --- packages/jellyfish-api-core/src/index.ts | 13 ++++++++----- packages/jellyfish-json/__tests__/index.test.ts | 12 +++++++++++- packages/jellyfish-json/__tests__/remap.test.ts | 4 ++-- packages/jellyfish-json/src/index.ts | 17 +++++++---------- packages/jellyfish-json/src/remap.ts | 14 +++++++------- website/docs/jellyfish/design.md | 2 ++ 6 files changed, 37 insertions(+), 25 deletions(-) diff --git a/packages/jellyfish-api-core/src/index.ts b/packages/jellyfish-api-core/src/index.ts index 4e3d5b29c0..b5086e61cc 100644 --- a/packages/jellyfish-api-core/src/index.ts +++ b/packages/jellyfish-api-core/src/index.ts @@ -1,4 +1,4 @@ -import { Precision, PrecisionMapping } from '@defichain/jellyfish-json' +import { Precision, PrecisionPath } from '@defichain/jellyfish-json' import { Blockchain } from './category/blockchain' import { Mining } from './category/mining' import { Wallet } from './category/wallet' @@ -19,9 +19,9 @@ export abstract class ApiClient { /** * A promise based procedure call handling * - * @param method Name of the RPC method - * @param params Array of params as RPC payload - * @param precision + * @param {string} method name of the RPC method + * @param {any[]} params array of params as RPC payload + * @param {Precision | PrecisionPath} precision * Numeric precision to parse RPC payload as 'lossless', 'bignumber' or 'number'. * * 'lossless' uses LosslessJSON that parses numeric values as LosslessNumber. With LosslessNumber, one can perform @@ -31,11 +31,14 @@ export abstract class ApiClient { * * 'number' parse all numeric values as 'Number' and precision will be loss if it exceeds IEEE-754 standard. * + * 'PrecisionPath' path based precision mapping, specifying 'bignumber' will automatically map all Number in that + * path as 'bignumber'. Otherwise, it will default to number, This applies deeply. + * * @throws ApiError * @throws RpcApiError * @throws ClientApiError */ - abstract call (method: string, params: any[], precision: Precision | PrecisionMapping): Promise + abstract call (method: string, params: any[], precision: Precision | PrecisionPath): Promise } /** diff --git a/packages/jellyfish-json/__tests__/index.test.ts b/packages/jellyfish-json/__tests__/index.test.ts index e1526f773f..a8acfd48b6 100644 --- a/packages/jellyfish-json/__tests__/index.test.ts +++ b/packages/jellyfish-json/__tests__/index.test.ts @@ -65,4 +65,14 @@ describe('stringify', () => { }) }) -// TODO(fuxingloh): mapping test +it('should remap object at root with precision', () => { + const parsed = JellyfishJSON.parse(`{ + "big": 10.4, + "num": 1234 + }`, { + big: 'bignumber' + }) + + expect(parsed.big instanceof BigNumber).toBe(true) + expect(parsed.num).toBe(1234) +}) diff --git a/packages/jellyfish-json/__tests__/remap.test.ts b/packages/jellyfish-json/__tests__/remap.test.ts index d26be7b631..c956d21529 100644 --- a/packages/jellyfish-json/__tests__/remap.test.ts +++ b/packages/jellyfish-json/__tests__/remap.test.ts @@ -1,5 +1,5 @@ import { parse } from 'lossless-json' -import { PrecisionMapping, remap } from '../src/remap' +import { PrecisionPath, remap } from '../src/remap' import { BigNumber } from '../src' it('should remap everything in the path', () => { @@ -56,7 +56,7 @@ it('should remap everything in the path', () => { expect(remapped.h).toBe(1.2) }) -function parseAndRemap (text: string, precision: PrecisionMapping): any { +function parseAndRemap (text: string, precision: PrecisionPath): any { const losslessObj = parse(text) return remap(losslessObj, precision) } diff --git a/packages/jellyfish-json/src/index.ts b/packages/jellyfish-json/src/index.ts index 42ecfe021a..11043e9c52 100644 --- a/packages/jellyfish-json/src/index.ts +++ b/packages/jellyfish-json/src/index.ts @@ -1,8 +1,8 @@ import BigNumber from 'bignumber.js' import { parse, stringify, LosslessNumber } from 'lossless-json' -import { PrecisionMapping, remap } from './remap' +import { PrecisionPath, remap } from './remap' -export { BigNumber, LosslessNumber, PrecisionMapping } +export { BigNumber, LosslessNumber, PrecisionPath } /** * Numeric precision to parse RPC payload as. @@ -36,16 +36,13 @@ export const JellyfishJSON = { /** * Precision parses all numeric value as the given Precision. * - * PrecisionMapping selectively remap each numeric value based on the mapping provided, + * PrecisionPath selectively remap each numeric value based on the mapping provided, * defaults to number if precision is not provided for the key. This works deeply. * - * PrecisionMapping will throw an error is there is Precision mismatch, as it is scanned deeply. - * Precision will not throw an error as it blindly remap all numeric value at root. - * - * @param text JSON string to parse into object. - * @param precision Numeric precision to parse payload as. + * @param {string} text JSON string to parse into object. + * @param {Precision | PrecisionPath} precision Numeric precision to parse payload as. */ - parse (text: string, precision: Precision | PrecisionMapping): any { + parse (text: string, precision: Precision | PrecisionPath): any { if (typeof precision === 'string') { switch (precision) { case 'lossless': @@ -67,7 +64,7 @@ export const JellyfishJSON = { }, /** - * @param value Object to stringify, with no risk of losing precision. + * @param {any} value object to stringify, with no risk of losing precision. */ stringify (value: any): string { const replacer = (key: string, value: any): any => { diff --git a/packages/jellyfish-json/src/remap.ts b/packages/jellyfish-json/src/remap.ts index 12a80e6b22..12cb540740 100644 --- a/packages/jellyfish-json/src/remap.ts +++ b/packages/jellyfish-json/src/remap.ts @@ -4,7 +4,7 @@ import { LosslessNumber } from 'lossless-json' /** * Path based precision mapping * Specifying 'bignumber' will automatically map all Number in that path as 'bignumber'. - * Otherwise it will default to number. This works deeply. + * Otherwise, it will default to number, This applies deeply. * * @example * path = { @@ -25,23 +25,23 @@ import { LosslessNumber } from 'lossless-json' * g: number * } */ -export interface PrecisionMapping { - [path: string]: 'bignumber' | PrecisionMapping +export interface PrecisionPath { + [path: string]: 'bignumber' | PrecisionPath } /** * @param {any} losslessObj to deeply remap into bignumber or number. - * @param {'bignumber' | PrecisionMapping} precision path mapping + * @param {'bignumber' | PrecisionPath} precision path mapping */ -export function remap (losslessObj: any, precision: PrecisionMapping): any { +export function remap (losslessObj: any, precision: PrecisionPath): any { return deepRemap(losslessObj, precision) } /** * @param {any} losslessObj to deeply remap - * @param {'bignumber' | PrecisionMapping} precision path mapping + * @param {'bignumber' | PrecisionPath} precision path mapping */ -function deepRemap (losslessObj: any, precision: 'bignumber' | PrecisionMapping): any { +function deepRemap (losslessObj: any, precision: 'bignumber' | PrecisionPath): any { if (typeof precision !== 'object') { return reviveObjectAs(losslessObj, precision) } diff --git a/website/docs/jellyfish/design.md b/website/docs/jellyfish/design.md index 0f667ed4b4..548041eadd 100644 --- a/website/docs/jellyfish/design.md +++ b/website/docs/jellyfish/design.md @@ -56,6 +56,8 @@ it('lost precision converting DFI 😥', () => { regular numeric operations, and it will throw an error when this would result in losing information. * **'bignumber'** parse all numeric values as 'BigNumber' using bignumber.js library. * **'number'** parse all numeric values as 'Number' and precision will be loss if it exceeds IEEE-754 standard. +* **'PrecisionPath'** path based precision mapping, specifying 'bignumber' will automatically map all Number in that + path as 'bignumber'. Otherwise, it will default to number, This applies deeply. As not all number parsed are significant in all context, (e.g. `mining.getMintingInfo()`), this allows jellyfish library From bb91e5e8a68295552c8dcf5b0e72d04ad1e1fe39 Mon Sep 17 00:00:00 2001 From: Fuxing Loh Date: Fri, 16 Apr 2021 16:27:12 +0800 Subject: [PATCH 3/3] apply & refactor jellyfish-json consumers --- .../__tests__/container_adapter_client.ts | 11 ++-- .../src/category/blockchain.ts | 6 +- packages/jellyfish-api-jsonrpc/src/index.ts | 9 ++- .../jellyfish-json/__tests__/remap.test.ts | 60 ++++++++++++++++++- packages/jellyfish-json/src/remap.ts | 43 +++++++++---- 5 files changed, 105 insertions(+), 24 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/container_adapter_client.ts b/packages/jellyfish-api-core/__tests__/container_adapter_client.ts index 4915c89b03..13019e3560 100644 --- a/packages/jellyfish-api-core/__tests__/container_adapter_client.ts +++ b/packages/jellyfish-api-core/__tests__/container_adapter_client.ts @@ -1,5 +1,6 @@ -import { JellyfishJSON, ApiClient, Precision, RpcApiError } from '../src' +import { ApiClient, RpcApiError } from '../src' import { DeFiDContainer } from '@defichain/testcontainers' +import { JellyfishJSON, Precision, PrecisionPath } from '@defichain/jellyfish-json' /** * Jellyfish client adapter for container @@ -16,7 +17,7 @@ export class ContainerAdapterClient extends ApiClient { /** * Wrap the call from client to testcontainers. */ - async call (method: string, params: any[], precision: Precision): Promise { + async call (method: string, params: any[], precision: Precision | PrecisionPath): Promise { const body = JellyfishJSON.stringify({ jsonrpc: '1.0', id: Math.floor(Math.random() * 100000000000000), @@ -25,9 +26,9 @@ export class ContainerAdapterClient extends ApiClient { }) const text = await this.container.post(body) - const response = JellyfishJSON.parse(text, precision) - - const { result, error } = response + const { result, error } = JellyfishJSON.parse(text, { + result: precision + }) if (error !== undefined && error !== null) { throw new RpcApiError(error) diff --git a/packages/jellyfish-api-core/src/category/blockchain.ts b/packages/jellyfish-api-core/src/category/blockchain.ts index ada36ea4a8..225c79f0ae 100644 --- a/packages/jellyfish-api-core/src/category/blockchain.ts +++ b/packages/jellyfish-api-core/src/category/blockchain.ts @@ -79,7 +79,11 @@ export class Blockchain { * @return Promise */ async getTxOut (txId: string, index: number, includeMempool = true): Promise { - return await this.client.call('gettxout', [txId, index, includeMempool], { value: 'bignumber' }) + return await this.client.call('gettxout', [ + txId, index, includeMempool + ], { + value: 'bignumber' + }) } } diff --git a/packages/jellyfish-api-jsonrpc/src/index.ts b/packages/jellyfish-api-jsonrpc/src/index.ts index 369f15724c..b3d727bbcf 100644 --- a/packages/jellyfish-api-jsonrpc/src/index.ts +++ b/packages/jellyfish-api-jsonrpc/src/index.ts @@ -8,6 +8,7 @@ import { import fetch from 'cross-fetch' import { Response } from 'cross-fetch/lib.fetch' import AbortController from 'abort-controller' +import { PrecisionPath } from '@defichain/jellyfish-json' /** * ClientOptions for JsonRpc @@ -58,7 +59,7 @@ export class JsonRpcClient extends ApiClient { /** * Implements JSON-RPC 1.0 specification for ApiClient */ - async call (method: string, params: any[], precision: Precision): Promise { + async call (method: string, params: any[], precision: Precision | PrecisionPath): Promise { const body = JsonRpcClient.stringify(method, params) const response = await this.fetchTimeout(body) const text = await response.text() @@ -83,8 +84,10 @@ export class JsonRpcClient extends ApiClient { }) } - private static parse (text: string, precision: Precision): any { - const { result, error } = JellyfishJSON.parse(text, precision) + private static parse (text: string, precision: Precision | PrecisionPath): any { + const { result, error } = JellyfishJSON.parse(text, { + result: precision + }) if (error !== undefined && error !== null) { throw new RpcApiError(error) diff --git a/packages/jellyfish-json/__tests__/remap.test.ts b/packages/jellyfish-json/__tests__/remap.test.ts index c956d21529..ef4ddd6f97 100644 --- a/packages/jellyfish-json/__tests__/remap.test.ts +++ b/packages/jellyfish-json/__tests__/remap.test.ts @@ -1,4 +1,4 @@ -import { parse } from 'lossless-json' +import { LosslessNumber, parse } from 'lossless-json' import { PrecisionPath, remap } from '../src/remap' import { BigNumber } from '../src' @@ -145,8 +145,28 @@ describe('remap individually', () => { }) }) +it('should remap lossless | number | bignumber', () => { + const parsed = parseAndRemap(`{ + "a": 1, + "b": 2, + "c": 3 + }`, { + a: 'bignumber', + b: 'lossless', + c: 'number' + }) + + expect(parsed.a instanceof BigNumber).toBe(true) + expect(parsed.a.toString()).toBe('1') + + expect(parsed.b instanceof LosslessNumber).toBe(true) + expect(parsed.b.toString()).toBe('2') + + expect(parsed.c).toBe(3) +}) + describe('remap invalid mapping should succeed', () => { - it('should ignore invalid remapping', () => { + it('should ignore invalid mapping', () => { const parsed = parseAndRemap(`{ "ignored": 123.4, "num": 1000 @@ -158,4 +178,40 @@ describe('remap invalid mapping should succeed', () => { expect(parsed.ignored).toBe(123.4) expect(parsed.num).toBe(1000) }) + + it('should ignore invalid null object', () => { + remap({ + invalid: null + }, { + invalid: 'bignumber' + }) + }) + + it('should ignore invalid undefined object', () => { + remap({ + invalid: undefined + }, { + invalid: 'bignumber' + }) + }) + + it('should ignore invalid null mapping', () => { + parseAndRemap(`{ + "invalid": 123.4, + "num": 1000 + }`, { + // @ts-expect-error + invalid: undefined + }) + }) + + it('should ignore invalid undefined mapping', () => { + parseAndRemap(`{ + "invalid": 123.4, + "num": 1000 + }`, { + // @ts-expect-error + invalid: undefined + }) + }) }) diff --git a/packages/jellyfish-json/src/remap.ts b/packages/jellyfish-json/src/remap.ts index 12cb540740..d14ed9c6f7 100644 --- a/packages/jellyfish-json/src/remap.ts +++ b/packages/jellyfish-json/src/remap.ts @@ -1,5 +1,6 @@ import BigNumber from 'bignumber.js' import { LosslessNumber } from 'lossless-json' +import { Precision } from './index' /** * Path based precision mapping @@ -26,7 +27,7 @@ import { LosslessNumber } from 'lossless-json' * } */ export interface PrecisionPath { - [path: string]: 'bignumber' | PrecisionPath + [path: string]: Precision | PrecisionPath } /** @@ -41,40 +42,52 @@ export function remap (losslessObj: any, precision: PrecisionPath): any { * @param {any} losslessObj to deeply remap * @param {'bignumber' | PrecisionPath} precision path mapping */ -function deepRemap (losslessObj: any, precision: 'bignumber' | PrecisionPath): any { +function deepRemap (losslessObj: any, precision: Precision | PrecisionPath): any { + if (losslessObj === null || losslessObj === undefined) { + return losslessObj + } + if (typeof precision !== 'object') { - return reviveObjectAs(losslessObj, precision) + return reviveAs(losslessObj, precision) } + if (Array.isArray(losslessObj)) { return losslessObj.map(obj => deepRemap(obj, precision)) } + if (losslessObj instanceof LosslessNumber) { + return reviveLosslessAs(losslessObj) + } + for (const [key, value] of Object.entries(losslessObj)) { losslessObj[key] = deepRemap(value, precision[key]) } - return reviveObjectAs(losslessObj) + return losslessObj } /** - * Array will deeply remapped, object keys will be iterated on. + * Array will deeply remapped, object keys will be iterated on as keys. * * @param {any} losslessObj to revive - * @param {'bignumber'} precision to use, specific 'bignumber' for BigNumber else always default to number + * @param precision to use, specific 'bignumber' for BigNumber or values always ignored and default to number */ -function reviveObjectAs (losslessObj: any, precision?: 'bignumber' | string): any { - if (Array.isArray(losslessObj)) { - return losslessObj.map((v: any) => reviveObjectAs(v, precision)) +function reviveAs (losslessObj: any, precision?: Precision): any { + if (losslessObj === null || losslessObj === undefined) { + return losslessObj } if (losslessObj instanceof LosslessNumber) { return reviveLosslessAs(losslessObj, precision) } + if (Array.isArray(losslessObj)) { + return losslessObj.map((v: any) => reviveAs(v, precision)) + } + if (typeof losslessObj === 'object') { for (const [key, value] of Object.entries(losslessObj)) { - losslessObj[key] = reviveObjectAs(value, precision) + losslessObj[key] = reviveAs(value, precision) } - return losslessObj } return losslessObj @@ -82,9 +95,13 @@ function reviveObjectAs (losslessObj: any, precision?: 'bignumber' | string): an /** * @param {LosslessNumber} losslessNum to revive as bignumber or number if precision != bignumber - * @param {'bignumber'} precision to use, specific 'bignumber' for BigNumber else always default to number + * @param {Precision} precision to use, specific 'bignumber' for BigNumber else always default to number */ -function reviveLosslessAs (losslessNum: LosslessNumber, precision?: 'bignumber' | string): BigNumber | number { +function reviveLosslessAs (losslessNum: LosslessNumber, precision?: Precision): BigNumber | LosslessNumber | number { + if (precision === 'lossless') { + return losslessNum + } + if (precision === 'bignumber') { return new BigNumber(losslessNum.toString()) }