Skip to content

Commit

Permalink
Fix format schema with list of objects (#7040)
Browse files Browse the repository at this point in the history
* Fix format schema with list of objects

* cover with test
  • Loading branch information
avkos committed May 22, 2024
1 parent ac2e180 commit e0fc158
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 94 deletions.
239 changes: 149 additions & 90 deletions packages/web3-utils/src/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
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<string, unknown> | 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
Expand All @@ -167,112 +276,62 @@ export const convert = (
}

const object = data as Record<string, unknown>;
// 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<string, unknown> | 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;
Expand Down
109 changes: 109 additions & 0 deletions packages/web3-utils/test/unit/formatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading

1 comment on commit e0fc158

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: e0fc158 Previous: ac2e180 Ratio
processingTx 9220 ops/sec (±3.39%) 8800 ops/sec (±4.35%) 0.95
processingContractDeploy 37127 ops/sec (±6.27%) 39867 ops/sec (±6.83%) 1.07
processingContractMethodSend 19169 ops/sec (±7.11%) 19416 ops/sec (±7.11%) 1.01
processingContractMethodCall 37740 ops/sec (±5.81%) 39762 ops/sec (±5.77%) 1.05
abiEncode 42093 ops/sec (±6.98%) 45021 ops/sec (±7.04%) 1.07
abiDecode 28578 ops/sec (±7.99%) 30398 ops/sec (±8.02%) 1.06
sign 1542 ops/sec (±0.87%) 1559 ops/sec (±0.80%) 1.01
verify 360 ops/sec (±0.57%) 367 ops/sec (±3.03%) 1.02

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.