Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix format schema with list of objects #7040

Merged
merged 5 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@

return value;
};

const convertArray = ({
value,
schemaProp,
schema,
object,
key,
dataPath,
format,
oneOfPath = [],

Check warning on line 157 in packages/web3-utils/src/formatter.ts

View check run for this annotation

Codecov / codecov/patch

packages/web3-utils/src/formatter.ts#L157

Added line #L157 was not covered by tests
}: {
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'))

Check warning on line 189 in packages/web3-utils/src/formatter.ts

View check run for this annotation

Codecov / codecov/patch

packages/web3-utils/src/formatter.ts#L183-L189

Added lines #L183 - L189 were not covered by tests
) {
_schemaProp = oneOfSchemaProp;
oneOfPath.push([key, index]);

Check warning on line 192 in packages/web3-utils/src/formatter.ts

View check run for this annotation

Codecov / codecov/patch

packages/web3-utils/src/formatter.ts#L191-L192

Added lines #L191 - L192 were not covered by tests
}
});
}

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();

Check warning on line 201 in packages/web3-utils/src/formatter.ts

View check run for this annotation

Codecov / codecov/patch

packages/web3-utils/src/formatter.ts#L200-L201

Added lines #L200 - L201 were not covered by tests

return true;

Check warning on line 203 in packages/web3-utils/src/formatter.ts

View check run for this annotation

Codecov / codecov/patch

packages/web3-utils/src/formatter.ts#L203

Added line #L203 was not covered by tests
}

// 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 @@
}

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
Loading