Skip to content

Commit

Permalink
fix(exo): Extend InterfaceGuard to support symbol-keyed methods
Browse files Browse the repository at this point in the history
Fixes #1728
  • Loading branch information
gibson042 committed Aug 27, 2023
1 parent e6d8a7a commit d6ea36b
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 14 deletions.
10 changes: 9 additions & 1 deletion packages/exo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ 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';

...
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);
const methodNames = [
...Reflect.ownKeys(interfaceGuard.methodGuards),
...(interfaceGuard.symbolMethodGuards
? [...getCopyMapEntries(interfaceGuard.symbolMethodGuards)].map(
entry => entry[0],
)
: []),
];
...
```
16 changes: 13 additions & 3 deletions packages/exo/src/exo-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import {
isAwaitArgGuard,
assertMethodGuard,
assertInterfaceGuard,
getCopyMapEntries,
} from '@endo/patterns';

/** @typedef {import('@endo/patterns').Method} Method */
/** @typedef {import('@endo/patterns').MethodGuard} MethodGuard */

const { quote: q, Fail } = assert;
const { apply, ownKeys } = Reflect;
const { defineProperties } = Object;
const { defineProperties, fromEntries } = Object;

/**
* A method guard, for inclusion in an interface guard, that enforces only that
Expand Down Expand Up @@ -263,8 +264,17 @@ export const defendPrototype = (
let methodGuards;
if (interfaceGuard) {
assertInterfaceGuard(interfaceGuard);
const { interfaceName, methodGuards: mg, sloppy = false } = interfaceGuard;
methodGuards = mg;
const {
interfaceName,
methodGuards: mg,
symbolMethodGuards,
sloppy = false,
} = interfaceGuard;
methodGuards = harden({
...mg,
...(symbolMethodGuards &&
fromEntries(getCopyMapEntries(symbolMethodGuards))),
});
{
const methodNames = ownKeys(behaviorMethods);
const methodGuardNames = ownKeys(methodGuards);
Expand Down
24 changes: 23 additions & 1 deletion packages/exo/test/test-heap-classes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { test } from './prepare-test-env-ava.js';

// eslint-disable-next-line import/order
import { M } from '@endo/patterns';
import { getCopyMapEntries, M } from '@endo/patterns';
import {
defineExoClass,
defineExoClassKit,
Expand Down Expand Up @@ -53,6 +53,28 @@ test('test defineExoClass', t => {
});
t.deepEqual(upCounter[GET_INTERFACE_GUARD](), UpCounterI);
t.deepEqual(ownKeys(UpCounterI.methodGuards), ['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')],
);
const makeFoo = defineExoClass('Foo', FooI, () => ({}), {
m() {},
[symbolic]() {},
});
const foo = makeFoo();
t.deepEqual(foo[GET_INTERFACE_GUARD](), FooI);
t.throws(() => foo[symbolic]('invalid arg'), {
message:
'In "[Symbol(symbolic)]" method of (Foo): arg 0: string "invalid arg" - Must be a boolean',
});
});

test('test defineExoClassKit', t => {
Expand Down
38 changes: 31 additions & 7 deletions packages/patterns/src/patterns/patternMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
checkCopyMap,
copyMapKeySet,
checkCopyBag,
makeCopyMap,
} from '../keys/checkKey.js';

import './internal-types.js';
Expand Down Expand Up @@ -1804,12 +1805,17 @@ const makeMethodGuardMaker = (
},
});

const InterfaceGuardShape = harden({
klass: 'Interface',
interfaceName: M.string(),
methodGuards: M.recordOf(M.string(), MethodGuardShape),
sloppy: M.boolean(),
});
const InterfaceGuardShape = M.splitRecord(
{
klass: 'Interface',
interfaceName: M.string(),
methodGuards: M.recordOf(M.string(), MethodGuardShape),
sloppy: M.boolean(),
},
{
symbolMethodGuards: M.mapOf(M.symbol(), MethodGuardShape),
},
);

export const assertInterfaceGuard = specimen => {
mustMatch(specimen, InterfaceGuardShape, 'interfaceGuard');
Expand All @@ -1824,11 +1830,29 @@ harden(assertInterfaceGuard);
*/
const makeInterfaceGuard = (interfaceName, methodGuards, options = {}) => {
const { sloppy = false } = options;
// For backwards compatibility, string-keyed method guards are represented in
// a CopyRecord. But symbol-keyed methods cannot be, so we put those in a
// CopyMap when present.
/** @type {Record<string, MethodGuard>} */
const stringMethodGuards = {};
/** @type {Array<[symbol, MethodGuard]>} */
const symbolMethodGuardsEntries = [];
for (const key of ownKeys(methodGuards)) {
const value = methodGuards[/** @type {string} */ (key)];
if (typeof key === 'symbol') {
symbolMethodGuardsEntries.push([key, value]);
} else {
stringMethodGuards[key] = value;
}
}
/** @type {InterfaceGuard} */
const result = harden({
klass: 'Interface',
interfaceName,
methodGuards,
methodGuards: stringMethodGuards,
...(symbolMethodGuardsEntries.length
? { symbolMethodGuards: makeCopyMap(symbolMethodGuardsEntries) }
: {}),
sloppy,
});
assertInterfaceGuard(result);
Expand Down
5 changes: 3 additions & 2 deletions packages/patterns/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,9 @@ export {};
* @typedef {{
* klass: 'Interface',
* interfaceName: string,
* methodGuards: T
* sloppy?: boolean
* methodGuards: { [K in keyof T]: K extends symbol ? never : T[K] },
* symbolMethodGuards?: CopyMap<(keyof T) & symbol, MethodGuard>,
* sloppy?: boolean,
* }} InterfaceGuard
*
* TODO https://github.com/endojs/endo/pull/1712 to make it into a genuine
Expand Down

0 comments on commit d6ea36b

Please sign in to comment.