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

Commit 0e57ecd

Browse files
authored
Add type guard utilities (#91)
Adds some type guard utilities in a new file, `utils.ts`, and accompanying unit tests. In addition to fixing #89, adds a function for validating JSON-RPC ID values. As part of implementing that, this also fixes a long-standing bug where we returned `undefined` instead of `null` for the ID when returning an error for a request without an `id` value.
1 parent a49cc10 commit 0e57ecd

File tree

5 files changed

+210
-3
lines changed

5 files changed

+210
-3
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- `isJsonRpcSuccess` and `isJsonRpcFailure` type guard utilities ([#91](https://github.com/MetaMask/json-rpc-engine/pull/91))
13+
- JSON-RPC ID validation utility and type guard, via `getJsonRpcIdValidator` ([#91](https://github.com/MetaMask/json-rpc-engine/pull/91))
14+
15+
### Changed
16+
17+
- **(BREAKING)** Return a `null` instead of `undefined` response `id` for malformed request objects ([#91](https://github.com/MetaMask/json-rpc-engine/pull/91))
18+
- This is very unlikely to be breaking in practice, but the behavior could have been relied on.
19+
1020
## [6.1.0] - 2020-11-20
1121

1222
### Added

src/JsonRpcEngine.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type JsonRpcVersion = '2.0';
2323
* notification. The value SHOULD normally not be Null and Numbers SHOULD
2424
* NOT contain fractional parts.
2525
*/
26-
export type JsonRpcId = number | string | void;
26+
export type JsonRpcId = number | string | null;
2727

2828
export interface JsonRpcError {
2929
code: number;
@@ -274,7 +274,7 @@ export class JsonRpcEngine extends SafeEventEmitter {
274274
`Requests must be plain objects. Received: ${typeof callerReq}`,
275275
{ request: callerReq },
276276
);
277-
return cb(error, { id: undefined, jsonrpc: '2.0', error });
277+
return cb(error, { id: null, jsonrpc: '2.0', error });
278278
}
279279

280280
if (typeof callerReq.method !== 'string') {

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
export * from './idRemapMiddleware';
21
export * from './createAsyncMiddleware';
32
export * from './createScaffoldMiddleware';
43
export * from './getUniqueId';
4+
export * from './idRemapMiddleware';
55
export * from './JsonRpcEngine';
66
export * from './mergeMiddleware';
7+
export * from './utils';

src/utils.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type {
2+
JsonRpcFailure,
3+
JsonRpcId,
4+
JsonRpcResponse,
5+
JsonRpcSuccess,
6+
} from './JsonRpcEngine';
7+
8+
/**
9+
* **ATTN:** Assumes that only one of the `result` and `error` properties is
10+
* present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`.
11+
*
12+
* Type guard to narrow a JsonRpcResponse object to a success (or failure).
13+
*
14+
* @param response - The response object to check.
15+
* @returns Whether the response object is a success, i.e. has a `result`
16+
* property.
17+
*/
18+
export function isJsonRpcSuccess<T>(
19+
response: JsonRpcResponse<T>,
20+
): response is JsonRpcSuccess<T> {
21+
return 'result' in response;
22+
}
23+
24+
/**
25+
* **ATTN:** Assumes that only one of the `result` and `error` properties is
26+
* present on the `response`, as guaranteed by e.g. `JsonRpcEngine.handle`.
27+
*
28+
* Type guard to narrow a JsonRpcResponse object to a failure (or success).
29+
*
30+
* @param response - The response object to check.
31+
* @returns Whether the response object is a failure, i.e. has an `error`
32+
* property.
33+
*/
34+
export function isJsonRpcFailure(
35+
response: JsonRpcResponse<unknown>,
36+
): response is JsonRpcFailure {
37+
return 'error' in response;
38+
}
39+
40+
interface JsonRpcValidatorOptions {
41+
permitEmptyString?: boolean;
42+
permitFractions?: boolean;
43+
permitNull?: boolean;
44+
}
45+
46+
const DEFAULT_VALIDATOR_OPTIONS: JsonRpcValidatorOptions = {
47+
permitEmptyString: true,
48+
permitFractions: false,
49+
permitNull: true,
50+
};
51+
52+
/**
53+
* Gets a function for validating JSON-RPC request / response `id` values.
54+
*
55+
* By manipulating the options of this factory, you can control the behavior
56+
* of the resulting validator for some edge cases. This is useful because e.g.
57+
* `null` should sometimes but not always be permitted.
58+
*
59+
* Note that the empty string (`''`) is always permitted by the JSON-RPC
60+
* specification, but that kind of sucks and you may want to forbid it in some
61+
* instances anyway.
62+
*
63+
* For more details, see the
64+
* [JSON-RPC Specification](https://www.jsonrpc.org/specification).
65+
*
66+
* @param options - An options object.
67+
* @param options.permitEmptyString - Whether the empty string (i.e. `''`)
68+
* should be treated as a valid ID. Default: `true`
69+
* @param options.permitFractions - Whether fractional numbers (e.g. `1.2`)
70+
* should be treated as valid IDs. Default: `false`
71+
* @param options.permitNull - Whether `null` should be treated as a valid ID.
72+
* Default: `true`
73+
* @returns The JSON-RPC ID validator function.
74+
*/
75+
export function getJsonRpcIdValidator(options?: JsonRpcValidatorOptions) {
76+
const { permitEmptyString, permitFractions, permitNull } = {
77+
...DEFAULT_VALIDATOR_OPTIONS,
78+
...options,
79+
};
80+
81+
/**
82+
* @param id - The JSON-RPC ID value to check.
83+
* @returns Whether the given ID is valid per the options given to the
84+
* factory.
85+
*/
86+
const isValidJsonRpcId = (id: unknown): id is JsonRpcId => {
87+
return Boolean(
88+
(typeof id === 'number' && (permitFractions || Number.isInteger(id))) ||
89+
(typeof id === 'string' && (permitEmptyString || id.length > 0)) ||
90+
(permitNull && id === null),
91+
);
92+
};
93+
return isValidJsonRpcId;
94+
}

test/utils.test.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* eslint-env mocha */
2+
'use strict';
3+
4+
const { strict: assert } = require('assert');
5+
const {
6+
isJsonRpcFailure,
7+
isJsonRpcSuccess,
8+
getJsonRpcIdValidator,
9+
} = require('../dist');
10+
11+
describe('isJsonRpcSuccess', function () {
12+
it('correctly identifies JSON-RPC response objects', function () {
13+
assert.equal(isJsonRpcSuccess({ result: 'success' }), true);
14+
assert.equal(isJsonRpcSuccess({ result: null }), true);
15+
assert.equal(isJsonRpcSuccess({ error: new Error('foo') }), false);
16+
assert.equal(isJsonRpcSuccess({}), false);
17+
});
18+
});
19+
20+
describe('isJsonRpcFailure', function () {
21+
it('correctly identifies JSON-RPC response objects', function () {
22+
assert.equal(isJsonRpcFailure({ error: 'failure' }), true);
23+
assert.equal(isJsonRpcFailure({ error: null }), true);
24+
assert.equal(isJsonRpcFailure({ result: 'success' }), false);
25+
assert.equal(isJsonRpcFailure({}), false);
26+
});
27+
});
28+
29+
describe('getJsonRpcIdValidator', function () {
30+
const getInputs = () => {
31+
return {
32+
// invariant with respect to options
33+
fractionString: { value: '1.2', expected: true },
34+
negativeInteger: { value: -1, expected: true },
35+
object: { value: {}, expected: false },
36+
positiveInteger: { value: 1, expected: true },
37+
string: { value: 'foo', expected: true },
38+
undefined: { value: undefined, expected: false },
39+
zero: { value: 0, expected: true },
40+
// variant with respect to options
41+
emptyString: { value: '', expected: true },
42+
fraction: { value: 1.2, expected: false },
43+
null: { value: null, expected: true },
44+
};
45+
};
46+
47+
const validateAll = (validate, inputs) => {
48+
for (const input of Object.values(inputs)) {
49+
assert.equal(
50+
validate(input.value),
51+
input.expected,
52+
`should output "${input.expected}" for "${input.value}"`,
53+
);
54+
}
55+
};
56+
57+
it('performs as expected with default options', function () {
58+
const inputs = getInputs();
59+
60+
// The default options are:
61+
// permitEmptyString: true,
62+
// permitFractions: false,
63+
// permitNull: true,
64+
validateAll(getJsonRpcIdValidator(), inputs);
65+
});
66+
67+
it('performs as expected with "permitEmptyString: false"', function () {
68+
const inputs = getInputs();
69+
inputs.emptyString.expected = false;
70+
71+
validateAll(
72+
getJsonRpcIdValidator({
73+
permitEmptyString: false,
74+
}),
75+
inputs,
76+
);
77+
});
78+
79+
it('performs as expected with "permitFractions: true"', function () {
80+
const inputs = getInputs();
81+
inputs.fraction.expected = true;
82+
83+
validateAll(
84+
getJsonRpcIdValidator({
85+
permitFractions: true,
86+
}),
87+
inputs,
88+
);
89+
});
90+
91+
it('performs as expected with "permitNull: false"', function () {
92+
const inputs = getInputs();
93+
inputs.null.expected = false;
94+
95+
validateAll(
96+
getJsonRpcIdValidator({
97+
permitNull: false,
98+
}),
99+
inputs,
100+
);
101+
});
102+
});

0 commit comments

Comments
 (0)