From e0fc158e7c18ab97b66e415653faad46491823c0 Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Wed, 22 May 2024 11:57:14 -0400 Subject: [PATCH] Fix format schema with list of objects (#7040) * Fix format schema with list of objects * cover with test --- packages/web3-utils/src/formatter.ts | 239 +++++++++++------- .../web3-utils/test/unit/formatter.test.ts | 109 ++++++++ .../test/unit/promise_helpers.test.ts | 15 +- 3 files changed, 269 insertions(+), 94 deletions(-) diff --git a/packages/web3-utils/src/formatter.ts b/packages/web3-utils/src/formatter.ts index 0a8ca3b723f..42062add5c6 100644 --- a/packages/web3-utils/src/formatter.ts +++ b/packages/web3-utils/src/formatter.ts @@ -145,6 +145,115 @@ export const convertScalarValue = (value: unknown, ethType: string, format: Data return value; }; + +const convertArray = ({ + value, + schemaProp, + schema, + object, + key, + dataPath, + format, + oneOfPath = [], +}: { + value: unknown; + schemaProp: JsonSchema; + schema: JsonSchema; + object: Record; + key: string; + dataPath: string[]; + format: DataFormat; + oneOfPath: [string, number][]; +}) => { + // If value is an array + if (Array.isArray(value)) { + let _schemaProp = schemaProp; + + // TODO This is a naive approach to solving the issue of + // a schema using oneOf. This chunk of code was intended to handle + // BlockSchema.transactions + // TODO BlockSchema.transactions are not being formatted + if (schemaProp?.oneOf !== undefined) { + // The following code is basically saying: + // if the schema specifies oneOf, then we are to loop + // over each possible schema and check if they type of the schema + // matches the type of value[0], and if so we use the oneOfSchemaProp + // as the schema for formatting + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + schemaProp.oneOf.forEach((oneOfSchemaProp: JsonSchema, index: number) => { + if ( + !Array.isArray(schemaProp?.items) && + ((typeof value[0] === 'object' && + (oneOfSchemaProp?.items as JsonSchema)?.type === 'object') || + (typeof value[0] === 'string' && + (oneOfSchemaProp?.items as JsonSchema)?.type !== 'object')) + ) { + _schemaProp = oneOfSchemaProp; + oneOfPath.push([key, index]); + } + }); + } + + if (isNullish(_schemaProp?.items)) { + // Can not find schema for array item, delete that item + // eslint-disable-next-line no-param-reassign + delete object[key]; + dataPath.pop(); + + return true; + } + + // If schema for array items is a single type + if (isObject(_schemaProp.items) && !isNullish(_schemaProp.items.format)) { + for (let i = 0; i < value.length; i += 1) { + // eslint-disable-next-line no-param-reassign + (object[key] as unknown[])[i] = convertScalarValue( + value[i], + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + _schemaProp?.items?.format, + format, + ); + } + + dataPath.pop(); + return true; + } + + // If schema for array items is an object + if (!Array.isArray(_schemaProp?.items) && _schemaProp?.items?.type === 'object') { + for (const arrObject of value) { + // eslint-disable-next-line no-use-before-define + convert( + arrObject as Record | unknown[], + schema, + dataPath, + format, + oneOfPath, + ); + } + + dataPath.pop(); + return true; + } + + // If schema for array is a tuple + if (Array.isArray(_schemaProp?.items)) { + for (let i = 0; i < value.length; i += 1) { + // eslint-disable-next-line no-param-reassign + (object[key] as unknown[])[i] = convertScalarValue( + value[i], + _schemaProp.items[i].format as string, + format, + ); + } + + dataPath.pop(); + return true; + } + } + return false; +}; + /** * Converts the data to the specified format * @param data - data to convert @@ -167,112 +276,62 @@ export const convert = ( } const object = data as Record; + // case when schema is array and `items` is object + if ( + Array.isArray(object) && + schema?.type === 'array' && + (schema?.items as JsonSchema)?.type === 'object' + ) { + convertArray({ + value: object, + schemaProp: schema, + schema, + object, + key: '', + dataPath, + format, + oneOfPath, + }); + } else { + for (const [key, value] of Object.entries(object)) { + dataPath.push(key); + const schemaProp = findSchemaByDataPath(schema, dataPath, oneOfPath); - for (const [key, value] of Object.entries(object)) { - dataPath.push(key); - const schemaProp = findSchemaByDataPath(schema, dataPath, oneOfPath); - - // If value is a scaler value - if (isNullish(schemaProp)) { - delete object[key]; - dataPath.pop(); - - continue; - } - - // If value is an object, recurse into it - if (isObject(value)) { - convert(value, schema, dataPath, format); - dataPath.pop(); - continue; - } - - // If value is an array - if (Array.isArray(value)) { - let _schemaProp = schemaProp; - - // TODO This is a naive approach to solving the issue of - // a schema using oneOf. This chunk of code was intended to handle - // BlockSchema.transactions - // TODO BlockSchema.transactions are not being formatted - if (schemaProp?.oneOf !== undefined) { - // The following code is basically saying: - // if the schema specifies oneOf, then we are to loop - // over each possible schema and check if they type of the schema - // matches the type of value[0], and if so we use the oneOfSchemaProp - // as the schema for formatting - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - schemaProp.oneOf.forEach((oneOfSchemaProp: JsonSchema, index: number) => { - if ( - !Array.isArray(schemaProp?.items) && - ((typeof value[0] === 'object' && - (oneOfSchemaProp?.items as JsonSchema)?.type === 'object') || - (typeof value[0] === 'string' && - (oneOfSchemaProp?.items as JsonSchema)?.type !== 'object')) - ) { - _schemaProp = oneOfSchemaProp; - oneOfPath.push([key, index]); - } - }); - } - - if (isNullish(_schemaProp?.items)) { - // Can not find schema for array item, delete that item + // If value is a scaler value + if (isNullish(schemaProp)) { delete object[key]; dataPath.pop(); continue; } - // If schema for array items is a single type - if (isObject(_schemaProp.items) && !isNullish(_schemaProp.items.format)) { - for (let i = 0; i < value.length; i += 1) { - (object[key] as unknown[])[i] = convertScalarValue( - value[i], - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - _schemaProp?.items?.format, - format, - ); - } - + // If value is an object, recurse into it + if (isObject(value)) { + convert(value, schema, dataPath, format); dataPath.pop(); continue; } - // If schema for array items is an object - if (!Array.isArray(_schemaProp?.items) && _schemaProp?.items?.type === 'object') { - for (const arrObject of value) { - convert( - arrObject as Record | unknown[], - schema, - dataPath, - format, - oneOfPath, - ); - } - - dataPath.pop(); + // If value is an array + if ( + convertArray({ + value, + schemaProp, + schema, + object, + key, + dataPath, + format, + oneOfPath, + }) + ) { continue; } - // If schema for array is a tuple - if (Array.isArray(_schemaProp?.items)) { - for (let i = 0; i < value.length; i += 1) { - (object[key] as unknown[])[i] = convertScalarValue( - value[i], - _schemaProp.items[i].format as string, - format, - ); - } + object[key] = convertScalarValue(value, schemaProp.format as string, format); - dataPath.pop(); - continue; - } + dataPath.pop(); } - - object[key] = convertScalarValue(value, schemaProp.format as string, format); - - dataPath.pop(); } return object; diff --git a/packages/web3-utils/test/unit/formatter.test.ts b/packages/web3-utils/test/unit/formatter.test.ts index b58f5459584..bed8b55de8e 100644 --- a/packages/web3-utils/test/unit/formatter.test.ts +++ b/packages/web3-utils/test/unit/formatter.test.ts @@ -519,6 +519,115 @@ describe('formatter', () => { ).toEqual(result); }); + it('should format array of objects', () => { + const schema = { + type: 'array', + items: { + type: 'object', + properties: { + prop1: { + format: 'uint', + }, + prop2: { + format: 'bytes', + }, + }, + }, + }; + + const data = [ + { prop1: 10, prop2: new Uint8Array(hexToBytes('FF')) }, + { prop1: 10, prop2: new Uint8Array(hexToBytes('FF')) }, + ]; + + const result = [ + { prop1: '0xa', prop2: '0xff' }, + { prop1: '0xa', prop2: '0xff' }, + ]; + + expect( + format(schema, data, { number: FMT_NUMBER.HEX, bytes: FMT_BYTES.HEX }), + ).toEqual(result); + }); + + it('should format array of objects with oneOf', () => { + const schema = { + type: 'array', + items: { + type: 'object', + properties: { + prop1: { + oneOf: [{ format: 'address' }, { type: 'string' }], + }, + prop2: { + format: 'bytes', + }, + }, + }, + }; + + const data = [ + { + prop1: '0x7ed0e85b8e1e925600b4373e6d108f34ab38a401', + prop2: new Uint8Array(hexToBytes('FF')), + }, + { prop1: 'some string', prop2: new Uint8Array(hexToBytes('FF')) }, + ]; + + const result = [ + { prop1: '0x7ed0e85b8e1e925600b4373e6d108f34ab38a401', prop2: '0xff' }, + { prop1: 'some string', prop2: '0xff' }, + ]; + + expect( + format(schema, data, { number: FMT_NUMBER.HEX, bytes: FMT_BYTES.HEX }), + ).toEqual(result); + }); + + it('should format array of different objects', () => { + const schema = { + type: 'array', + items: [ + { + type: 'object', + properties: { + prop1: { + format: 'uint', + }, + prop2: { + format: 'bytes', + }, + }, + }, + { + type: 'object', + properties: { + prop1: { + format: 'string', + }, + prop2: { + format: 'uint', + }, + }, + }, + ], + }; + + const data = [ + { prop1: 10, prop2: new Uint8Array(hexToBytes('FF')) }, + { prop1: 'test', prop2: 123 }, + ]; + + const result = [ + { prop1: 10, prop2: '0xff' }, + { prop1: 'test', prop2: 123 }, + ]; + + expect( + format(schema, data, { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }), + ).toEqual(result); + }); + it('should format array values with object type', () => { const schema = { type: 'object', diff --git a/packages/web3-utils/test/unit/promise_helpers.test.ts b/packages/web3-utils/test/unit/promise_helpers.test.ts index b18f4f1a5b0..7492c51fc24 100644 --- a/packages/web3-utils/test/unit/promise_helpers.test.ts +++ b/packages/web3-utils/test/unit/promise_helpers.test.ts @@ -147,20 +147,26 @@ describe('promise helpers', () => { }); it('should return interval id if not resolved in specific time', async () => { - let counter = 0; + // eslint-disable-next-line @typescript-eslint/require-await const asyncHelper = async () => { if (counter <= 3000000) { counter += 1; return undefined; } - return "result"; + return 'result'; }; const testError = new Error('Test P2 Error'); - const [neverResolvePromise, intervalId] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100); - const promiCheck = Promise.race([neverResolvePromise, rejectIfTimeout(500,testError)[1]]); + const [neverResolvePromise, intervalId] = pollTillDefinedAndReturnIntervalId( + asyncHelper, + 100, + ); + const promiCheck = Promise.race([ + neverResolvePromise, + rejectIfTimeout(500, testError)[1], + ]); await expect(promiCheck).rejects.toThrow(testError); expect(intervalId).toBeDefined(); @@ -188,6 +194,7 @@ describe('promise helpers', () => { it('reject if later throws', async () => { const dummyError = new Error('error'); let counter = 0; + // eslint-disable-next-line @typescript-eslint/require-await const asyncHelper = async () => { if (counter === 0) { counter += 1;