diff --git a/src/__fixtures__/json.ts b/src/__fixtures__/json.ts index 12edfab3..ca869f5a 100644 --- a/src/__fixtures__/json.ts +++ b/src/__fixtures__/json.ts @@ -671,6 +671,325 @@ export const JSON_RPC_RESPONSE_FIXTURES = { ], }; +export const JSON_RPC_PENDING_RESPONSE_FIXTURES = { + valid: [ + ...JSON_RPC_SUCCESS_FIXTURES.valid, + ...JSON_RPC_FAILURE_FIXTURES.valid, + { + id: 1, + jsonrpc: '2.0', + }, + { + id: 1, + jsonrpc: '2.0', + error: undefined, + }, + { + id: 1, + jsonrpc: '2.0', + result: undefined, + }, + { + id: 1, + jsonrpc: '2.0', + result: undefined, + error: undefined, + }, + { + id: 1, + jsonrpc: '2.0', + result: { + foo: 'bar', + }, + error: { + code: -32000, + message: 'Internal error', + }, + }, + ], + invalid: [ + {}, + [], + true, + false, + null, + undefined, + 1, + 'foo', + { + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: {}, + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: [], + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: true, + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: false, + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: undefined, + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '1.0', + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: 2.0, + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: {}, + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: [], + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: true, + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: false, + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: null, + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: undefined, + error: { + code: -32000, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: [], + }, + { + id: 1, + jsonrpc: '2.0', + error: {}, + }, + { + id: 1, + jsonrpc: '2.0', + error: true, + }, + { + id: 1, + jsonrpc: '2.0', + error: false, + }, + { + id: 1, + jsonrpc: '2.0', + error: null, + }, + { + id: 1, + jsonrpc: '2.0', + error: 'foo', + }, + { + id: 1, + jsonrpc: '2.0', + error: 1, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: {}, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: [], + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: true, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: false, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: null, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: undefined, + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: 'foo', + message: 'Internal error', + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: {}, + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: [], + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: true, + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: false, + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: null, + }, + }, + { + id: 1, + jsonrpc: '2.0', + error: { + code: -32000, + message: undefined, + }, + }, + ], +}; + export const COMPLEX_OBJECT = { data: { account: { diff --git a/src/json.test.ts b/src/json.test.ts index a0917763..05bd02e8 100644 --- a/src/json.test.ts +++ b/src/json.test.ts @@ -6,6 +6,7 @@ import { JSON_FIXTURES, JSON_RPC_FAILURE_FIXTURES, JSON_RPC_NOTIFICATION_FIXTURES, + JSON_RPC_PENDING_RESPONSE_FIXTURES, JSON_RPC_REQUEST_FIXTURES, JSON_RPC_RESPONSE_FIXTURES, JSON_RPC_SUCCESS_FIXTURES, @@ -18,12 +19,14 @@ import { assertIsJsonRpcRequest, assertIsJsonRpcResponse, assertIsJsonRpcSuccess, + assertIsPendingJsonRpcResponse, getJsonRpcIdValidator, isJsonRpcFailure, isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, isJsonRpcSuccess, + isPendingJsonRpcResponse, isValidJson, validateJsonAndGetSize, } from '.'; @@ -254,6 +257,50 @@ describe('json', () => { }); }); + describe('isPendingJsonRpcResponse', () => { + it.each(JSON_RPC_PENDING_RESPONSE_FIXTURES.valid)( + 'returns true for a valid pending JSON-RPC response', + (response) => { + expect(isPendingJsonRpcResponse(response)).toBe(true); + }, + ); + + it.each(JSON_RPC_PENDING_RESPONSE_FIXTURES.invalid)( + 'returns false for an invalid pending JSON-RPC response', + (response) => { + expect(isPendingJsonRpcResponse(response)).toBe(false); + }, + ); + }); + + describe('assertIsPendingJsonRpcResponse', () => { + it.each(JSON_RPC_PENDING_RESPONSE_FIXTURES.valid)( + 'does not throw for a valid pending JSON-RPC response', + (response) => { + expect(() => assertIsPendingJsonRpcResponse(response)).not.toThrow(); + }, + ); + + it.each(JSON_RPC_PENDING_RESPONSE_FIXTURES.invalid)( + 'throws for an invalid pending JSON-RPC response', + (response) => { + expect(() => assertIsPendingJsonRpcResponse(response)).toThrow( + 'Not a pending JSON-RPC response', + ); + }, + ); + + it('includes the value thrown in the message if it is not an error', () => { + jest.spyOn(superstructModule, 'assert').mockImplementation(() => { + throw 'oops'; + }); + + expect(() => + assertIsPendingJsonRpcResponse(JSON_RPC_FAILURE_FIXTURES.invalid[0]), + ).toThrow('Not a pending JSON-RPC response: oops'); + }); + }); + describe('isJsonRpcResponse', () => { it.each(JSON_RPC_RESPONSE_FIXTURES.valid)( 'returns true for a valid JSON-RPC response', diff --git a/src/json.ts b/src/json.ts index 2680142b..f1aaaff9 100644 --- a/src/json.ts +++ b/src/json.ts @@ -162,8 +162,8 @@ export type JsonRpcNotification = InferWithParams< >; /** - * Type guard to narrow a JSON-RPC request or notification object to a - * notification. + * Type guard to narrow a {@link JsonRpcRequest} or + * {@link JsonRpcNotification} object to a {@link JsonRpcNotification}. * * @param requestOrNotification - The JSON-RPC request or notification to check. * @returns Whether the specified JSON-RPC message is a notification. @@ -175,8 +175,8 @@ export function isJsonRpcNotification( } /** - * Assertion type guard to narrow a JSON-RPC request or notification object to a - * notification. + * Assertion type guard to narrow a {@link JsonRpcRequest} or + * {@link JsonRpcNotification} object to a {@link JsonRpcNotification}. * * @param requestOrNotification - The JSON-RPC request or notification to check. */ @@ -192,7 +192,8 @@ export function assertIsJsonRpcNotification( } /** - * Type guard to narrow a JSON-RPC request or notification object to a request. + * Type guard to narrow a {@link JsonRpcRequest} or @link JsonRpcNotification} + * object to a {@link JsonRpcRequest}. * * @param requestOrNotification - The JSON-RPC request or notification to check. * @returns Whether the specified JSON-RPC message is a request. @@ -204,8 +205,8 @@ export function isJsonRpcRequest( } /** - * Assertion type guard to narrow a JSON-RPC request or notification object to a - * request. + * Assertion type guard to narrow a {@link JsonRpcRequest} or + * {@link JsonRpcNotification} object to a {@link JsonRpcRequest}. * * @param requestOrNotification - The JSON-RPC request or notification to check. */ @@ -220,6 +221,23 @@ export function assertIsJsonRpcRequest( } } +export const PendingJsonRpcResponseStruct = object({ + id: JsonRpcIdStruct, + jsonrpc: JsonRpcVersionStruct, + result: optional(unknown()), + error: optional(JsonRpcErrorStruct), +}); + +/** + * A JSON-RPC response object that has not yet been resolved. + */ +export type PendingJsonRpcResponse = Omit< + Infer, + 'result' +> & { + result?: Result; +}; + export const JsonRpcSuccessStruct = object({ id: JsonRpcIdStruct, jsonrpc: JsonRpcVersionStruct, @@ -263,7 +281,38 @@ export type JsonRpcResponse = | JsonRpcFailure; /** - * Type guard to check if a value is a JsonRpcResponse. + * Type guard to check whether specified JSON-RPC response is a + * {@link PendingJsonRpcResponse}. + * + * @param response - The JSON-RPC response to check. + * @returns Whether the specified JSON-RPC response is pending. + */ +export function isPendingJsonRpcResponse( + response: unknown, +): response is PendingJsonRpcResponse { + return is(response, PendingJsonRpcResponseStruct); +} + +/** + * Assert that the specified JSON-RPC response is a + * {@link PendingJsonRpcResponse}. + * + * @param response - The JSON-RPC response to check. + * @throws If the specified JSON-RPC response is not pending. + */ +export function assertIsPendingJsonRpcResponse( + response: unknown, +): asserts response is PendingJsonRpcResponse { + try { + assert(response, PendingJsonRpcResponseStruct); + } catch (error) { + const message = isErrorWithMessage(error) ? error.message : error; + throw new Error(`Not a pending JSON-RPC response: ${message}.`); + } +} + +/** + * Type guard to check if a value is a {@link JsonRpcResponse}. * * @param response - The object to check. * @returns Whether the object is a JsonRpcResponse. @@ -275,7 +324,7 @@ export function isJsonRpcResponse( } /** - * Type assertion to check if a value is a JsonRpcResponse. + * Type assertion to check if a value is a {@link JsonRpcResponse}. * * @param response - The response to check. */ @@ -291,7 +340,8 @@ export function assertIsJsonRpcResponse( } /** - * Type guard to narrow a JsonRpcResponse object to a success (or failure). + * Type guard to narrow a {@link JsonRpcResponse} object to a success + * (or failure). * * @param response - The response object to check. * @returns Whether the response object is a success. @@ -303,7 +353,8 @@ export function isJsonRpcSuccess( } /** - * Type assertion to narrow a JsonRpcResponse object to a success (or failure). + * Type assertion to narrow a {@link JsonRpcResponse} object to a success + * (or failure). * * @param response - The response object to check. */ @@ -319,7 +370,8 @@ export function assertIsJsonRpcSuccess( } /** - * Type guard to narrow a JsonRpcResponse object to a failure (or success). + * Type guard to narrow a {@link JsonRpcResponse} object to a failure + * (or success). * * @param response - The response object to check. * @returns Whether the response object is a failure, i.e. has an `error` @@ -332,7 +384,8 @@ export function isJsonRpcFailure( } /** - * Type assertion to narrow a JsonRpcResponse object to a failure (or success). + * Type assertion to narrow a {@link JsonRpcResponse} object to a failure + * (or success). * * @param response - The response object to check. */