From 5c315eb9014d3d512e54420e400643ad5124ac24 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:51:19 -0500 Subject: [PATCH 1/3] feat(utils): Add some fundamental type guards. --- packages/utils/package.json | 4 ++ packages/utils/src/index.test.ts | 9 +++- packages/utils/src/index.ts | 2 + packages/utils/src/types.test.ts | 87 ++++++++++++++++++++++++++++++++ packages/utils/src/types.ts | 27 ++++++++++ yarn.lock | 2 + 6 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 packages/utils/src/types.test.ts create mode 100644 packages/utils/src/types.ts diff --git a/packages/utils/package.json b/packages/utils/package.json index ddb46dd55..3ece22cca 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -41,6 +41,10 @@ "test:verbose": "yarn test --reporter verbose", "test:watch": "vitest --config vitest.config.ts" }, + "dependencies": { + "@endo/captp": "^4.2.2", + "@metamask/utils": "^9.1.0" + }, "devDependencies": { "@arethetypeswrong/cli": "^0.15.3", "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/utils/src/index.test.ts b/packages/utils/src/index.test.ts index 5b2e188a5..ca15974fb 100644 --- a/packages/utils/src/index.test.ts +++ b/packages/utils/src/index.test.ts @@ -5,7 +5,14 @@ import * as indexModule from './index.js'; describe('index', () => { it('has the expected exports', () => { expect(Object.keys(indexModule)).toStrictEqual( - expect.arrayContaining(['makeCounter', 'makeLogger', 'stringify']), + expect.arrayContaining([ + 'makeCounter', + 'makeLogger', + 'stringify', + 'isPrimitive', + 'isTypedArray', + 'isTypedObject', + ]), ); }); }); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 60d4cea24..4d9a599c4 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -2,3 +2,5 @@ export type { Logger } from './logger.js'; export { makeLogger } from './logger.js'; export { makeCounter } from './counter.js'; export { stringify } from './stringify.js'; +export type { TypeGuard } from './types.js'; +export { isPrimitive, isTypedArray, isTypedObject } from './types.js'; diff --git a/packages/utils/src/types.test.ts b/packages/utils/src/types.test.ts new file mode 100644 index 000000000..2d35a8634 --- /dev/null +++ b/packages/utils/src/types.test.ts @@ -0,0 +1,87 @@ +import { isObject } from '@metamask/utils'; +import { describe, it, expect } from 'vitest'; + +import { isPrimitive, isTypedArray, isTypedObject } from './types.js'; + +const isNumber = (value: unknown): value is number => typeof value === 'number'; +const alwaysFalse = (): boolean => false; +const alwaysTrue = (): boolean => true; + +describe('isPrimitive', () => { + it.each` + value + ${''} + ${'foo'} + ${0} + ${6.28} + ${BigInt('9999999999999999')} + ${Symbol('meaning')} + ${false} + ${null} + ${undefined} + `('returns true for primitive $value', ({ value }) => { + expect(isPrimitive(value)).toBe(true); + }); + + it.each` + value + ${[]} + ${{}} + ${{ foo: 'bar' }} + ${new MessageChannel()} + ${alwaysTrue} + ${function foo() { + return 'bar'; +}} + `('returns false for invalid values: $value', ({ value }) => { + expect(isPrimitive(value)).toBe(false); + }); +}); + +describe('isTypedArray', () => { + it.each` + value | guard + ${[]} | ${alwaysFalse} + ${[0, 2, 4.5]} | ${isNumber} + ${[0, 'foo']} | ${isPrimitive} + ${[{}, { foo: 'bar' }]} | ${isObject} + ${[[]]} | ${Array.isArray} + `('returns true for homogeneously typed array $value', ({ value, guard }) => { + expect(isTypedArray(value, guard)).toBe(true); + }); + + it.each` + value | guard + ${[null]} | ${alwaysFalse} + ${0} | ${isNumber} + ${null} | ${alwaysTrue} + ${[0, 'foo']} | ${isNumber} + ${[0, [1]]} | ${isNumber} + ${[{}, 1]} | ${isObject} + `('returns false for invalid values: $value', ({ value, guard }) => { + expect(isTypedArray(value, guard)).toBe(false); + }); +}); + +describe('isTypedObject', () => { + it.each` + value | guard + ${{}} | ${alwaysFalse} + ${{ foo: 0, bar: 2 }} | ${isNumber} + ${{ foo: {}, bar: { foo: 0 } }} | ${isObject} + `( + 'returns true for homogeneously typed object $value', + ({ value, guard }) => { + expect(isTypedObject(value, guard)).toBe(true); + }, + ); + + it.each` + value | guard + ${{ foo: 'bar' }} | ${alwaysFalse} + ${null} | ${alwaysTrue} + ${[{}, { foo: 'bar ' }]} | ${isObject} + `('returns false for invalid values: $value', ({ value, guard }) => { + expect(isTypedObject(value, guard)).toBe(false); + }); +}); diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts new file mode 100644 index 000000000..4cdf01a17 --- /dev/null +++ b/packages/utils/src/types.ts @@ -0,0 +1,27 @@ +import type { Primitive } from '@endo/captp'; +import { isObject } from '@metamask/utils'; + +export type TypeGuard = (value: unknown) => value is Type; + +const primitives = [ + 'string', + 'number', + 'bigint', + 'boolean', + 'symbol', + 'null', + 'undefined', +]; +export const isPrimitive = (value: unknown): value is Primitive => + value === null || primitives.includes(typeof value); + +export const isTypedArray = ( + value: unknown, + isElement: TypeGuard, +): value is ElementType[] => Array.isArray(value) && value.every(isElement); + +export const isTypedObject = ( + value: unknown, + isValue: TypeGuard, +): value is { [Key in keyof object]: ValueType } => + isObject(value) && Object.values(value).every(isValue); diff --git a/yarn.lock b/yarn.lock index d4e76eb2c..db95450ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1607,10 +1607,12 @@ __metadata: resolution: "@ocap/utils@workspace:packages/utils" dependencies: "@arethetypeswrong/cli": "npm:^0.15.3" + "@endo/captp": "npm:^4.2.2" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/eslint-config": "npm:^13.0.0" "@metamask/eslint-config-nodejs": "npm:^13.0.0" "@metamask/eslint-config-typescript": "npm:^13.0.0" + "@metamask/utils": "npm:^9.1.0" "@ts-bridge/cli": "npm:^0.5.1" "@ts-bridge/shims": "npm:^0.1.1" "@typescript-eslint/eslint-plugin": "npm:^8.1.0" From 157e71ac336780181960082c4e963afcece88059 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:57:48 -0500 Subject: [PATCH 2/3] improve isCommandLike strength --- packages/kernel/src/command.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/kernel/src/command.ts b/packages/kernel/src/command.ts index de287c6e1..051d9b4d6 100644 --- a/packages/kernel/src/command.ts +++ b/packages/kernel/src/command.ts @@ -1,5 +1,6 @@ import type { Primitive } from '@endo/captp'; import { hasProperty, isObject } from '@metamask/utils'; +import { isPrimitive, isTypedArray, isTypedObject } from '@ocap/utils'; export enum CommandMethod { CapTpCall = 'callCapTp', @@ -16,6 +17,12 @@ export type CommandParams = | CommandParams[] | { [key: string]: CommandParams }; +const isCommandParams = (value: unknown): value is CommandParams => + isPrimitive(value) || + value instanceof Promise || + isTypedArray(value, isCommandParams) || + isTypedObject(value, isCommandParams); + export type CapTpPayload = { method: string; params: CommandParams[]; @@ -48,7 +55,8 @@ const isCommandLike = ( } => isObject(value) && Object.values(CommandMethod).includes(value.method as CommandMethod) && - hasProperty(value, 'params'); + hasProperty(value, 'params') && + isCommandParams(value.params); export type Command = | CommandLike From 0d2d329c28e250f5d04532e134d084729d27ae0a Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:23:51 -0500 Subject: [PATCH 3/3] short circuit element tests --- packages/utils/src/types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 4cdf01a17..da984ee0c 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -18,10 +18,11 @@ export const isPrimitive = (value: unknown): value is Primitive => export const isTypedArray = ( value: unknown, isElement: TypeGuard, -): value is ElementType[] => Array.isArray(value) && value.every(isElement); +): value is ElementType[] => + Array.isArray(value) && !value.some((ele) => !isElement(ele)); export const isTypedObject = ( value: unknown, isValue: TypeGuard, ): value is { [Key in keyof object]: ValueType } => - isObject(value) && Object.values(value).every(isValue); + isObject(value) && !Object.values(value).some((val) => !isValue(val));