mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-21 14:47:12 +01:00
fix(schema-utils): improve union errors, add weak assert
This commit is contained in:
committed by
Tomáš Martykán
parent
02ab9b9436
commit
23b664a894
@@ -1,7 +1,10 @@
|
||||
export class InvalidParameter extends Error {
|
||||
field?: string;
|
||||
import { ValueErrorType } from '@sinclair/typebox/errors';
|
||||
|
||||
constructor(reason: string, field: string, value?: any) {
|
||||
export class InvalidParameter extends Error {
|
||||
field: string;
|
||||
type: ValueErrorType;
|
||||
|
||||
constructor(reason: string, field: string, type: ValueErrorType, value?: any) {
|
||||
let message = `Invalid parameter`;
|
||||
message += ` "${field.substring(1)}"`;
|
||||
message += ` (= ${JSON.stringify(value)})`;
|
||||
@@ -9,5 +12,6 @@ export class InvalidParameter extends Error {
|
||||
super(message);
|
||||
this.name = 'InvalidParameter';
|
||||
this.field = field;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { JavaScriptTypeBuilder, Static, TSchema, TObject, Optional } from '@sinclair/typebox';
|
||||
import { ValueErrorType, Errors } from '@sinclair/typebox/errors';
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { JavaScriptTypeBuilder, Static, TSchema, TObject, Optional, Kind } from '@sinclair/typebox';
|
||||
import { ValueErrorType, Errors, ValueError } from '@sinclair/typebox/errors';
|
||||
import { Mixin } from 'ts-mixer';
|
||||
|
||||
import { ArrayBufferBuilder, BufferBuilder, KeyofEnumBuilder, UintBuilder } from './custom-types';
|
||||
@@ -15,7 +16,6 @@ class CustomTypeBuilder extends Mixin(
|
||||
|
||||
export function Validate<T extends TSchema>(schema: T, value: unknown): value is Static<T> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
Assert(schema, value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -23,6 +23,41 @@ export function Validate<T extends TSchema>(schema: T, value: unknown): value is
|
||||
}
|
||||
}
|
||||
|
||||
function FindErrorInUnion(error: ValueError) {
|
||||
const currentValue: any = error.value;
|
||||
const unionMembers: TSchema[] = error.schema.anyOf;
|
||||
const hasValidMember = unionMembers.find(unionSchema => Validate(unionSchema, currentValue));
|
||||
if (!hasValidMember) {
|
||||
// Find possible matches by literals
|
||||
const possibleMatchesByLiterals = unionMembers.filter(unionSchema => {
|
||||
if (unionSchema[Kind] !== 'Object') return false;
|
||||
return !Object.entries(unionSchema.properties as TObject['properties']).find(
|
||||
([property, propertySchema]) =>
|
||||
propertySchema.const && propertySchema.const !== currentValue[property],
|
||||
);
|
||||
});
|
||||
if (possibleMatchesByLiterals.length === 1) {
|
||||
// There is only one possible match
|
||||
Assert(possibleMatchesByLiterals[0], currentValue);
|
||||
} else if (possibleMatchesByLiterals.length > 1) {
|
||||
// Find match with least amount of errors
|
||||
const errorsOfPossibleMatches = possibleMatchesByLiterals.map(
|
||||
(matchSchema: TSchema) => ({
|
||||
schema: matchSchema,
|
||||
errors: [...Errors(matchSchema, currentValue)],
|
||||
}),
|
||||
);
|
||||
const sortedErrors = errorsOfPossibleMatches.sort(
|
||||
(a, b) => a.errors.length - b.errors.length,
|
||||
);
|
||||
const [bestMatch] = sortedErrors;
|
||||
Assert(bestMatch.schema, currentValue);
|
||||
}
|
||||
|
||||
throw new InvalidParameter(error.message, error.path, error.type, error.value);
|
||||
}
|
||||
}
|
||||
|
||||
export function Assert<T extends TSchema>(schema: T, value: unknown): asserts value is Static<T> {
|
||||
const errors = [...Errors(schema, value)];
|
||||
let [error] = errors;
|
||||
@@ -33,19 +68,34 @@ export function Assert<T extends TSchema>(schema: T, value: unknown): asserts va
|
||||
// Optional can also accept null values
|
||||
} else if (error.type === ValueErrorType.Union) {
|
||||
// Drill down into the union
|
||||
const currentValue = error.value;
|
||||
const hasValidMember = error.schema.anyOf.find((unionSchema: TSchema) =>
|
||||
Validate(unionSchema, currentValue),
|
||||
);
|
||||
if (!hasValidMember) throw new InvalidParameter(error.message, error.path, error.value);
|
||||
FindErrorInUnion(error);
|
||||
} else {
|
||||
throw new InvalidParameter(error.message, error.path, error.value);
|
||||
throw new InvalidParameter(error.message, error.path, error.type, error.value);
|
||||
}
|
||||
errors.shift();
|
||||
[error] = errors;
|
||||
}
|
||||
}
|
||||
|
||||
export function AssertWeak<T extends TSchema>(
|
||||
schema: T,
|
||||
value: unknown,
|
||||
): asserts value is Static<T> {
|
||||
try {
|
||||
Assert(schema, value);
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidParameter) {
|
||||
if (e.type === ValueErrorType.ObjectRequiredProperty) {
|
||||
// We consider this error to be serious
|
||||
throw e;
|
||||
}
|
||||
console.warn('Method params validation failed', e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const Type = new CustomTypeBuilder();
|
||||
export { Optional };
|
||||
export type { Static, TObject, TSchema };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Type, Validate } from '../src';
|
||||
import { Assert, Type, Validate } from '../src';
|
||||
|
||||
describe('complex-example', () => {
|
||||
it('should work with a schema like StellarSignTx', () => {
|
||||
@@ -76,4 +76,56 @@ describe('complex-example', () => {
|
||||
};
|
||||
expect(Validate(schema, invalidValue)).toBe(false);
|
||||
});
|
||||
|
||||
it('should work with union types', () => {
|
||||
const schemaA = Type.Object({
|
||||
type: Type.Literal('A'),
|
||||
a: Type.String(),
|
||||
});
|
||||
|
||||
const schemaA2 = Type.Object({
|
||||
type: Type.Literal('A'),
|
||||
b: Type.Number(),
|
||||
c: Type.Number(),
|
||||
});
|
||||
|
||||
const schemaB = Type.Object({
|
||||
type: Type.Literal('B'),
|
||||
b: Type.String(),
|
||||
});
|
||||
|
||||
const schema = Type.Union([schemaA, schemaA2, schemaB]);
|
||||
|
||||
const invalidSchema = {
|
||||
type: 'C',
|
||||
};
|
||||
expect(() => Assert(schema, invalidSchema)).toThrow(
|
||||
'Invalid parameter "" (= {"type":"C"}): Expected union value',
|
||||
);
|
||||
|
||||
const invalidSchema2 = {
|
||||
type: 'A',
|
||||
a: 123,
|
||||
};
|
||||
expect(() => Assert(schema, invalidSchema2)).toThrow(
|
||||
'Invalid parameter "a" (= 123): Expected string',
|
||||
);
|
||||
|
||||
const invalidSchema3 = {
|
||||
type: 'A',
|
||||
b: 123,
|
||||
c: 'str',
|
||||
};
|
||||
expect(() => Assert(schema, invalidSchema3)).toThrow(
|
||||
'Invalid parameter "c" (= "str"): Expected number',
|
||||
);
|
||||
|
||||
const invalidSchema4 = {
|
||||
type: 'B',
|
||||
a: 123,
|
||||
};
|
||||
expect(() => Assert(schema, invalidSchema4)).toThrow(
|
||||
'Invalid parameter "b" (= undefined): Required property',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
43
packages/schema-utils/tests/weak-assert.test.ts
Normal file
43
packages/schema-utils/tests/weak-assert.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { AssertWeak, Type } from '../src';
|
||||
|
||||
describe('weak-assert', () => {
|
||||
it('should not throw if type is mismatched', () => {
|
||||
const schema = Type.Object({
|
||||
foo: Type.String(),
|
||||
});
|
||||
const value = {
|
||||
foo: 123,
|
||||
};
|
||||
|
||||
console.warn = jest.fn();
|
||||
expect(() => AssertWeak(schema, value)).not.toThrow();
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not throw if a property is mismatched in a union', () => {
|
||||
const schema = Type.Union([
|
||||
Type.Object({
|
||||
foo: Type.String(),
|
||||
}),
|
||||
Type.Object({
|
||||
bar: Type.Number(),
|
||||
}),
|
||||
]);
|
||||
const value = {
|
||||
foo: 123,
|
||||
};
|
||||
|
||||
console.warn = jest.fn();
|
||||
expect(() => AssertWeak(schema, value)).not.toThrow();
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw if a required field is missing', () => {
|
||||
const schema = Type.Object({
|
||||
foo: Type.String(),
|
||||
});
|
||||
const value = {};
|
||||
|
||||
expect(() => AssertWeak(schema, value)).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user