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

Add JSON-RPC error validation functions #46

Merged
merged 1 commit into from Nov 1, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/__fixtures__/json.ts
Expand Up @@ -660,6 +660,76 @@ export const JSON_RPC_FAILURE_FIXTURES = {
],
};

export const JSON_RPC_ERROR_FIXTURES = {
valid: JSON_RPC_FAILURE_FIXTURES.valid.map((fixture) => fixture.error),
invalid: [
{},
[],
true,
false,
null,
undefined,
1,
'foo',
{
code: {},
message: 'Internal error',
},
{
code: [],
message: 'Internal error',
},
{
code: true,
message: 'Internal error',
},
{
code: false,
message: 'Internal error',
},
{
code: null,
message: 'Internal error',
},
{
code: undefined,
message: 'Internal error',
},
{
code: 'foo',
message: 'Internal error',
},
{
code: -32000,
message: {},
},
{
code: -32000,
message: [],
},
{
code: -32000,
message: true,
},
{
code: -32000,
message: false,
},
{
code: -32000,
message: null,
},
{
code: -32000,
message: undefined,
},
{
code: -32000.5,
message: undefined,
},
],
};

export const JSON_RPC_RESPONSE_FIXTURES = {
valid: [
...JSON_RPC_SUCCESS_FIXTURES.valid,
Expand Down
55 changes: 55 additions & 0 deletions src/json.test.ts
Expand Up @@ -4,6 +4,7 @@ import {
ARRAY_OF_MIXED_SPECIAL_OBJECTS,
COMPLEX_OBJECT,
JSON_FIXTURES,
JSON_RPC_ERROR_FIXTURES,
JSON_RPC_FAILURE_FIXTURES,
JSON_RPC_NOTIFICATION_FIXTURES,
JSON_RPC_PENDING_RESPONSE_FIXTURES,
Expand All @@ -29,6 +30,8 @@ import {
isPendingJsonRpcResponse,
isValidJson,
validateJsonAndGetSize,
isJsonRpcError,
assertIsJsonRpcError,
} from '.';

describe('json', () => {
Expand Down Expand Up @@ -257,6 +260,58 @@ describe('json', () => {
});
});

describe('isJsonRpcError', () => {
it.each(JSON_RPC_ERROR_FIXTURES.valid)(
'returns true for a valid JSON-RPC error',
(error) => {
expect(isJsonRpcError(error)).toBe(true);
},
);

it.each(JSON_RPC_ERROR_FIXTURES.invalid)(
'returns false for an invalid JSON-RPC error',
(error) => {
expect(isJsonRpcError(error)).toBe(false);
},
);
});

describe('assertIsJsonRpcError', () => {
it.each(JSON_RPC_ERROR_FIXTURES.valid)(
'does not throw an error for valid JSON-RPC error',
(error) => {
expect(() => assertIsJsonRpcError(error)).not.toThrow();
},
);

it.each(JSON_RPC_ERROR_FIXTURES.invalid)(
'throws an error for invalid JSON-RPC error',
(error) => {
expect(() => assertIsJsonRpcError(error)).toThrow(
'Not a JSON-RPC error',
);
},
);

it('includes the reason in the error message', () => {
expect(() =>
assertIsJsonRpcError(JSON_RPC_ERROR_FIXTURES.invalid[0]),
).toThrow(
'Not a JSON-RPC error: At path: code -- Expected an integer, but received: undefined.',
);
});

it('includes the value thrown in the message if it is not an error', () => {
jest.spyOn(superstructModule, 'assert').mockImplementation(() => {
throw 'oops';
});

expect(() =>
assertIsJsonRpcError(JSON_RPC_ERROR_FIXTURES.invalid[0]),
).toThrow('Not a JSON-RPC error: oops');
});
});

describe('isPendingJsonRpcResponse', () => {
it.each(JSON_RPC_PENDING_RESPONSE_FIXTURES.valid)(
'returns true for a valid pending JSON-RPC response',
Expand Down
31 changes: 29 additions & 2 deletions src/json.ts
Expand Up @@ -4,6 +4,7 @@ import {
assert,
boolean,
Infer,
integer,
is,
lazy,
literal,
Expand Down Expand Up @@ -94,9 +95,9 @@ export const JsonRpcIdStruct = nullable(union([number(), string()]));
export type JsonRpcId = Infer<typeof JsonRpcIdStruct>;

export const JsonRpcErrorStruct = object({
code: number(),
code: integer(),
message: string(),
data: optional(unknown()),
data: optional(JsonStruct),
stack: optional(string()),
});

Expand Down Expand Up @@ -400,6 +401,32 @@ export function assertIsJsonRpcFailure(
}
}

/**
* Type guard to validate whether an object is a valid JSON-RPC error.
*
* @param value - The value object to check.
* @returns Whether the response object is a valid JSON-RPC error.
*/
export function isJsonRpcError(value: unknown): value is JsonRpcError {
return is(value, JsonRpcErrorStruct);
}

/**
* Type assertion to validate whether an object is a valid JSON-RPC error.
*
* @param value - The value object to check.
*/
export function assertIsJsonRpcError(
value: unknown,
): asserts value is JsonRpcError {
try {
assert(value, JsonRpcErrorStruct);
} catch (error) {
const message = isErrorWithMessage(error) ? error.message : error;
throw new Error(`Not a JSON-RPC error: ${message}.`);
}
}

type JsonRpcValidatorOptions = {
permitEmptyString?: boolean;
permitFractions?: boolean;
Expand Down