Skip to content
This repository has been archived by the owner on Nov 9, 2023. It is now read-only.

Add type guard utilities #91

Merged
merged 7 commits into from
May 18, 2021
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `isJsonRpcSuccess` and `isJsonRpcFailure` type guard utilities ([#91](https://github.com/MetaMask/json-rpc-engine/pull/91))
- JSON-RPC ID validation utility and type guard, via `getJsonRpcIdValidator` ([#91](https://github.com/MetaMask/json-rpc-engine/pull/91))

### Changed

- **(BREAKING)** Return a `null` instead of `undefined` response `id` for malformed request objects ([#91](https://github.com/MetaMask/json-rpc-engine/pull/91))
- This is very unlikely to be breaking in practice, but the behavior could have been relied on.

## [6.1.0] - 2020-11-20

### Added
Expand Down
4 changes: 2 additions & 2 deletions src/JsonRpcEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type JsonRpcVersion = '2.0';
* notification. The value SHOULD normally not be Null and Numbers SHOULD
* NOT contain fractional parts.
*/
export type JsonRpcId = number | string | void;
export type JsonRpcId = number | string | null;

export interface JsonRpcError {
code: number;
Expand Down Expand Up @@ -274,7 +274,7 @@ export class JsonRpcEngine extends SafeEventEmitter {
`Requests must be plain objects. Received: ${typeof callerReq}`,
{ request: callerReq },
);
return cb(error, { id: undefined, jsonrpc: '2.0', error });
return cb(error, { id: null, jsonrpc: '2.0', error });
}

if (typeof callerReq.method !== 'string') {
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './idRemapMiddleware';
export * from './createAsyncMiddleware';
export * from './createScaffoldMiddleware';
export * from './getUniqueId';
export * from './idRemapMiddleware';
export * from './JsonRpcEngine';
export * from './mergeMiddleware';
export * from './utils';
94 changes: 94 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type {
JsonRpcFailure,
JsonRpcId,
JsonRpcResponse,
JsonRpcSuccess,
} from './JsonRpcEngine';

/**
* **ATTN:** Assumes that only one of the `result` and `error` properties is
* present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`.
*
* Type guard to narrow a JsonRpcResponse object to a success (or failure).
*
* @param response - The response object to check.
* @returns Whether the response object is a success, i.e. has a `result`
* property.
*/
export function isJsonRpcSuccess<T>(
response: JsonRpcResponse<T>,
): response is JsonRpcSuccess<T> {
return 'result' in response;
}

/**
* **ATTN:** Assumes that only one of the `result` and `error` properties is
* present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`.
*
* Type guard to narrow a 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`
* property.
*/
export function isJsonRpcFailure(
response: JsonRpcResponse<unknown>,
): response is JsonRpcFailure {
return 'error' in response;
}

interface JsonRpcValidatorOptions {
permitEmptyString?: boolean;
permitFractions?: boolean;
permitNull?: boolean;
}

const DEFAULT_VALIDATOR_OPTIONS: JsonRpcValidatorOptions = {
permitEmptyString: true,
permitFractions: false,
permitNull: true,
};

/**
* Gets a function for validating JSON-RPC request / response `id` values.
*
* By manipulating the options of this factory, you can control the behavior
* of the resulting validator for some edge cases. This is useful because e.g.
* `null` should sometimes but not always be permitted.
*
* Note that the empty string (`''`) is always permitted by the JSON-RPC
* specification, but that kind of sucks and you may want to forbid it in some
* instances anyway.
*
* For more details, see the
* [JSON-RPC Specification](https://www.jsonrpc.org/specification).
*
* @param options - An options object.
* @param options.permitEmptyString - Whether the empty string (i.e. `''`)
* should be treated as a valid ID. Default: `true`
* @param options.permitFractions - Whether fractional numbers (e.g. `1.2`)
* should be treated as valid IDs. Default: `false`
* @param options.permitNull - Whether `null` should be treated as a valid ID.
* Default: `true`
* @returns The JSON-RPC ID validator function.
*/
export function getJsonRpcIdValidator(options?: JsonRpcValidatorOptions) {
const { permitEmptyString, permitFractions, permitNull } = {
...DEFAULT_VALIDATOR_OPTIONS,
...options,
};

/**
* @param id - The JSON-RPC ID value to check.
* @returns Whether the given ID is valid per the options given to the
* factory.
*/
const isValidJsonRpcId = (id: unknown): id is JsonRpcId => {
return Boolean(
(typeof id === 'number' && (permitFractions || Number.isInteger(id))) ||
(typeof id === 'string' && (permitEmptyString || id.length > 0)) ||
(permitNull && id === null),
);
};
return isValidJsonRpcId;
}
102 changes: 102 additions & 0 deletions test/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-env mocha */
'use strict';

const { strict: assert } = require('assert');
const {
isJsonRpcFailure,
isJsonRpcSuccess,
getJsonRpcIdValidator,
} = require('../dist');

describe('isJsonRpcSuccess', function () {
it('correctly identifies JSON-RPC response objects', function () {
assert.equal(isJsonRpcSuccess({ result: 'success' }), true);
assert.equal(isJsonRpcSuccess({ result: null }), true);
assert.equal(isJsonRpcSuccess({ error: new Error('foo') }), false);
assert.equal(isJsonRpcSuccess({}), false);
});
});

describe('isJsonRpcFailure', function () {
it('correctly identifies JSON-RPC response objects', function () {
assert.equal(isJsonRpcFailure({ error: 'failure' }), true);
assert.equal(isJsonRpcFailure({ error: null }), true);
assert.equal(isJsonRpcFailure({ result: 'success' }), false);
assert.equal(isJsonRpcFailure({}), false);
});
});

describe('getJsonRpcIdValidator', function () {
const getInputs = () => {
return {
// invariant with respect to options
fractionString: { value: '1.2', expected: true },
negativeInteger: { value: -1, expected: true },
object: { value: {}, expected: false },
positiveInteger: { value: 1, expected: true },
string: { value: 'foo', expected: true },
undefined: { value: undefined, expected: false },
zero: { value: 0, expected: true },
// variant with respect to options
emptyString: { value: '', expected: true },
fraction: { value: 1.2, expected: false },
null: { value: null, expected: true },
};
};

const validateAll = (validate, inputs) => {
for (const input of Object.values(inputs)) {
assert.equal(
validate(input.value),
input.expected,
`should output "${input.expected}" for "${input.value}"`,
);
}
};

it('performs as expected with default options', function () {
const inputs = getInputs();

// The default options are:
// permitEmptyString: true,
// permitFractions: false,
// permitNull: true,
validateAll(getJsonRpcIdValidator(), inputs);
});

it('performs as expected with "permitEmptyString: false"', function () {
const inputs = getInputs();
inputs.emptyString.expected = false;

validateAll(
getJsonRpcIdValidator({
permitEmptyString: false,
}),
inputs,
);
});

it('performs as expected with "permitFractions: true"', function () {
const inputs = getInputs();
inputs.fraction.expected = true;

validateAll(
getJsonRpcIdValidator({
permitFractions: true,
}),
inputs,
);
});

it('performs as expected with "permitNull: false"', function () {
const inputs = getInputs();
inputs.null.expected = false;

validateAll(
getJsonRpcIdValidator({
permitNull: false,
}),
inputs,
);
});
});