Skip to content

Commit

Permalink
fix(patterns,exo): abstract guard getters
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Aug 28, 2023
1 parent 7f1f331 commit 14b381a
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 44 deletions.
13 changes: 3 additions & 10 deletions packages/exo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,13 @@ When an exo is defined with an InterfaceGuard, the exo is augmented by default w
```js
// `GET_INTERFACE_GUARD` holds the name of the meta-method
import { GET_INTERFACE_GUARD } from '@endo/exo';
import { getCopyMapEntries } from '@endo/patterns';
import { getInterfaceMethodNames } from '@endo/patterns';

...
const interfaceGuard = await E(exo)[GET_INTERFACE_GUARD]();
// `methodNames` omits names of automatically added meta-methods like
// the value of `GET_INTERFACE_GUARD`.
// Others may also be omitted if `interfaceGuard.partial`
const methodNames = [
...Reflect.ownKeys(interfaceGuard.methodGuards),
...(interfaceGuard.symbolMethodGuards
? [...getCopyMapEntries(interfaceGuard.symbolMethodGuards)].map(
entry => entry[0],
)
: []),
];
// Others may also be omitted if allowed by interfaceGuard options
const methodNames = getInterfaceMethodNames(interfaceGuard);
...
```
50 changes: 28 additions & 22 deletions packages/exo/src/exo-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {
mustMatch,
M,
isAwaitArgGuard,
assertMethodGuard,
assertInterfaceGuard,
getAwaitArgGuardPayload,
getMethodGuardPayload,
getInterfaceGuardPayload,
getCopyMapEntries,
} from '@endo/patterns';

Expand All @@ -26,8 +27,8 @@ const { defineProperties, fromEntries } = Object;
*/
const MinMethodGuard = M.call().rest(M.any()).returns(M.any());

const defendSyncArgs = (args, methodGuard, label) => {
const { argGuards, optionalArgGuards, restArgGuard } = methodGuard;
const defendSyncArgs = (args, methodGuardPayload, label) => {
const { argGuards, optionalArgGuards, restArgGuard } = methodGuardPayload;
const paramsPattern = M.splitArray(
argGuards,
optionalArgGuards,
Expand All @@ -38,16 +39,16 @@ const defendSyncArgs = (args, methodGuard, label) => {

/**
* @param {Method} method
* @param {MethodGuard} methodGuard
* @param {MethodGuardPayload} methodGuardPayload
* @param {string} label
* @returns {Method}
*/
const defendSyncMethod = (method, methodGuard, label) => {
const { returnGuard } = methodGuard;
const defendSyncMethod = (method, methodGuardPayload, label) => {
const { returnGuard } = methodGuardPayload;
const { syncMethod } = {
// Note purposeful use of `this` and concise method syntax
syncMethod(...args) {
defendSyncArgs(harden(args), methodGuard, label);
defendSyncArgs(harden(args), methodGuardPayload, label);
const result = apply(method, this, args);
mustMatch(harden(result), returnGuard, `${label}: result`);
return result;
Expand All @@ -56,8 +57,12 @@ const defendSyncMethod = (method, methodGuard, label) => {
return syncMethod;
};

const desync = methodGuard => {
const { argGuards, optionalArgGuards = [], restArgGuard } = methodGuard;
const desync = methodGuardPayload => {
const {
argGuards,
optionalArgGuards = [],
restArgGuard,
} = methodGuardPayload;
!isAwaitArgGuard(restArgGuard) ||
Fail`Rest args may not be awaited: ${restArgGuard}`;
const rawArgGuards = [...argGuards, ...optionalArgGuards];
Expand All @@ -66,23 +71,23 @@ const desync = methodGuard => {
for (let i = 0; i < rawArgGuards.length; i += 1) {
const argGuard = rawArgGuards[i];
if (isAwaitArgGuard(argGuard)) {
rawArgGuards[i] = argGuard.argGuard;
rawArgGuards[i] = getAwaitArgGuardPayload(argGuard).argGuard;
awaitIndexes.push(i);
}
}
return {
awaitIndexes,
rawMethodGuard: {
rawMethodGuardPayload: {
argGuards: rawArgGuards.slice(0, argGuards.length),
optionalArgGuards: rawArgGuards.slice(argGuards.length),
restArgGuard,
},
};
};

const defendAsyncMethod = (method, methodGuard, label) => {
const { returnGuard } = methodGuard;
const { awaitIndexes, rawMethodGuard } = desync(methodGuard);
const defendAsyncMethod = (method, methodGuardPayload, label) => {
const { returnGuard } = methodGuardPayload;
const { awaitIndexes, rawMethodGuardPayload } = desync(methodGuardPayload);
const { asyncMethod } = {
// Note purposeful use of `this` and concise method syntax
asyncMethod(...args) {
Expand All @@ -93,7 +98,7 @@ const defendAsyncMethod = (method, methodGuard, label) => {
for (let j = 0; j < awaitIndexes.length; j += 1) {
rawArgs[awaitIndexes[j]] = awaitedArgs[j];
}
defendSyncArgs(rawArgs, rawMethodGuard, label);
defendSyncArgs(rawArgs, rawMethodGuardPayload, label);
return apply(method, this, rawArgs);
});
return E.when(resultP, result => {
Expand All @@ -112,13 +117,13 @@ const defendAsyncMethod = (method, methodGuard, label) => {
* @param {string} label
*/
const defendMethod = (method, methodGuard, label) => {
assertMethodGuard(methodGuard);
const { callKind } = methodGuard;
const methodGuardPayload = getMethodGuardPayload(methodGuard);
const { callKind } = methodGuardPayload;
if (callKind === 'sync') {
return defendSyncMethod(method, methodGuard, label);
return defendSyncMethod(method, methodGuardPayload, label);
} else {
assert(callKind === 'async');
return defendAsyncMethod(method, methodGuard, label);
return defendAsyncMethod(method, methodGuardPayload, label);
}
};

Expand Down Expand Up @@ -261,15 +266,16 @@ export const defendPrototype = (
// @ts-expect-error TS misses that hasOwn check makes this safe
behaviorMethods = harden(methods);
}
/** @type {Record<string | symbol, MethodGuard> | undefined} */
let methodGuards;
if (interfaceGuard) {
assertInterfaceGuard(interfaceGuard);
const {
interfaceName,
methodGuards: mg,
symbolMethodGuards,
sloppy = false,
} = interfaceGuard;
// @ts-expect-error "missing" type parameter
} = getInterfaceGuardPayload(interfaceGuard);
methodGuards = harden({
...mg,
...(symbolMethodGuards &&
Expand Down
12 changes: 3 additions & 9 deletions packages/exo/test/test-heap-classes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
import { test } from './prepare-test-env-ava.js';

// eslint-disable-next-line import/order
import { getCopyMapEntries, M } from '@endo/patterns';
import { getInterfaceMethodNames, M } from '@endo/patterns';
import {
defineExoClass,
defineExoClassKit,
makeExo,
} from '../src/exo-makers.js';
import { GET_INTERFACE_GUARD } from '../src/exo-tools.js';

const { ownKeys } = Reflect;

const UpCounterI = M.interface('UpCounter', {
incr: M.call()
// TODO M.number() should not be needed to get a better error message
Expand Down Expand Up @@ -52,19 +50,15 @@ test('test defineExoClass', t => {
'In "incr" method of (UpCounter): arg 0?: string "foo" - Must be a number',
});
t.deepEqual(upCounter[GET_INTERFACE_GUARD](), UpCounterI);
t.deepEqual(ownKeys(UpCounterI.methodGuards), ['incr']);
t.deepEqual(getInterfaceMethodNames(UpCounterI), ['incr']);
t.is(UpCounterI.symbolMethodGuards, undefined);

const symbolic = Symbol.for('symbolic');
const FooI = M.interface('Foo', {
m: M.call().returns(),
[symbolic]: M.call(M.boolean()).returns(),
});
t.deepEqual(ownKeys(FooI.methodGuards), ['m']);
t.deepEqual(
[...getCopyMapEntries(FooI.symbolMethodGuards)].map(entry => entry[0]),
[Symbol.for('symbolic')],
);
t.deepEqual(getInterfaceMethodNames(FooI), ['m', Symbol.for('symbolic')]);
const makeFoo = defineExoClass('Foo', FooI, () => ({}), {
m() {},
[symbolic]() {},
Expand Down
4 changes: 4 additions & 0 deletions packages/patterns/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ export {
mustMatch,
isAwaitArgGuard,
assertAwaitArgGuard,
getAwaitArgGuardPayload,
assertMethodGuard,
getMethodGuardPayload,
getInterfaceMethodNames,
assertInterfaceGuard,
getInterfaceGuardPayload,
} from './src/patterns/patternMatchers.js';

// ////////////////// Temporary, until these find their proper home ////////////
Expand Down
3 changes: 3 additions & 0 deletions packages/patterns/src/patterns/internal-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
/** @typedef {import('@endo/marshal').RankCompare} RankCompare */
/** @typedef {import('@endo/marshal').RankCover} RankCover */

/** @typedef {import('../types.js').AwaitArgGuardPayload} AwaitArgGuardPayload */
/** @typedef {import('../types.js').AwaitArgGuard} AwaitArgGuard */
/** @typedef {import('../types.js').ArgGuard} ArgGuard */
/** @typedef {import('../types.js').MethodGuardPayload} MethodGuardPayload */
/** @typedef {import('../types.js').MethodGuard} MethodGuard */
/** @typedef {import('../types.js').InterfaceGuardPayload} InterfaceGuardPayload */
/** @typedef {import('../types.js').InterfaceGuard} InterfaceGuard */
/** @typedef {import('../types.js').MethodGuardMaker0} MethodGuardMaker0 */

Expand Down
82 changes: 82 additions & 0 deletions packages/patterns/src/patterns/patternMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
copyMapKeySet,
checkCopyBag,
makeCopyMap,
getCopyMapKeys,
} from '../keys/checkKey.js';

import './internal-types.js';
Expand Down Expand Up @@ -1707,21 +1708,45 @@ const AwaitArgGuardShape = harden({
argGuard: M.pattern(),
});

/**
* @param {any} specimen
* @returns {specimen is AwaitArgGuard}
*/
export const isAwaitArgGuard = specimen =>
matches(specimen, AwaitArgGuardShape);
harden(isAwaitArgGuard);

/**
* @param {any} specimen
* @returns {asserts specimen is AwaitArgGuard}
*/
export const assertAwaitArgGuard = specimen => {
mustMatch(specimen, AwaitArgGuardShape, 'awaitArgGuard');
};
harden(assertAwaitArgGuard);

/**
* By using this abstraction rather than accessing the properties directly,
* we smooth the transition to https://github.com/endojs/endo/pull/1712
*
* @param {AwaitArgGuard} awaitArgGuard
* @returns {AwaitArgGuardPayload}
*/
export const getAwaitArgGuardPayload = awaitArgGuard => {
assertAwaitArgGuard(awaitArgGuard);
const { klass: _, ...payload } = awaitArgGuard;
/** @type {AwaitArgGuardPayload} */
// @ts-expect-error inference too weak to see this is ok.
return payload;
};

/**
* @param {Pattern} argPattern
* @returns {AwaitArgGuard}
*/
const makeAwaitArgGuard = argPattern => {
/** @type {AwaitArgGuard} */
// @ts-expect-error inference too weak to see it is ok
const result = harden({
klass: 'awaitArg',
argGuard: argPattern,
Expand Down Expand Up @@ -1755,11 +1780,30 @@ const AsyncMethodGuardShape = harden({

const MethodGuardShape = M.or(SyncMethodGuardShape, AsyncMethodGuardShape);

/**
* @param {any} specimen
* @returns {asserts specimen is MethodGuard}
*/
export const assertMethodGuard = specimen => {
mustMatch(specimen, MethodGuardShape, 'methodGuard');
};
harden(assertMethodGuard);

/**
* By using this abstraction rather than accessing the properties directly,
* we smooth the transition to https://github.com/endojs/endo/pull/1712
*
* @param {MethodGuard} methodGuard
* @returns {MethodGuardPayload}
*/
export const getMethodGuardPayload = methodGuard => {
assertMethodGuard(methodGuard);
const { klass: _, ...payload } = methodGuard;
/** @type {MethodGuardPayload} */
// @ts-expect-error inference too weak to see this is ok.
return payload;
};

/**
* @param {'sync'|'async'} callKind
* @param {ArgGuard[]} argGuards
Expand Down Expand Up @@ -1792,6 +1836,7 @@ const makeMethodGuardMaker = (
},
returns: (returnGuard = M.undefined()) => {
/** @type {MethodGuard} */
// @ts-expect-error inference too weak to see it is ok
const result = harden({
klass: 'methodGuard',
callKind,
Expand All @@ -1817,11 +1862,47 @@ const InterfaceGuardShape = M.splitRecord(
},
);

/**
* @param {any} specimen
* @returns {asserts specimen is InterfaceGuard}
*/
export const assertInterfaceGuard = specimen => {
mustMatch(specimen, InterfaceGuardShape, 'interfaceGuard');
};
harden(assertInterfaceGuard);

/**
* By using this abstraction rather than accessing the properties directly,
* we smooth the transition to https://github.com/endojs/endo/pull/1712
*
* @param {InterfaceGuard} interfaceGuard
* @returns {InterfaceGuardPayload}
*/
export const getInterfaceGuardPayload = interfaceGuard => {
assertInterfaceGuard(interfaceGuard);
const { klass: _, ...payload } = interfaceGuard;
/** @type {InterfaceGuardPayload} */
// @ts-expect-error inference too weak to see this is ok.
return payload;
};

const emptyCopyMap = makeCopyMap([]);

/**
* @param {InterfaceGuard} interfaceGuard
* @returns {(string | symbol)[]}
*/
export const getInterfaceMethodNames = interfaceGuard => {
const { methodGuards, symbolMethodGuards = emptyCopyMap } =
getInterfaceGuardPayload(interfaceGuard);
/** @type {(string | symbol)[]} */
// @ts-expect-error inference is too weak to see this is ok
return harden([
...Reflect.ownKeys(methodGuards),
...getCopyMapKeys(symbolMethodGuards),
]);
};

/**
* @param {string} interfaceName
* @param {Record<string, MethodGuard>} methodGuards
Expand All @@ -1846,6 +1927,7 @@ const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => {
}
}
/** @type {InterfaceGuard} */
// @ts-expect-error inference too weak to see it is ok
const result = harden({
klass: 'Interface',
interfaceName,
Expand Down
Loading

0 comments on commit 14b381a

Please sign in to comment.