This repository has been archived by the owner on Nov 9, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
5 changed files
with
210 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
}); | ||
}); |