diff --git a/packages/exo/test/test-legacy-guard-tolerance.js b/packages/exo/test/test-legacy-guard-tolerance.js index 048aa8f085..f944eed475 100644 --- a/packages/exo/test/test-legacy-guard-tolerance.js +++ b/packages/exo/test/test-legacy-guard-tolerance.js @@ -33,7 +33,7 @@ test('legacy guard tolerance', async t => { mg2, }); const lig = makeLegacyInterfaceGuard({ - intefaceName: 'Foo', + interfaceName: 'Foo', methodGuards: { mg1, mg2, @@ -49,9 +49,8 @@ test('legacy guard tolerance', async t => { argGuard: 88, }); // @ts-expect-error Legacy adaptor can be ill typed - t.throws(() => getAwaitArgGuardPayload(laag), { - message: - 'awaitArgGuard: copyRecord {"argGuard":88,"klass":"awaitArg"} - Must be a guard:awaitArgGuard', + t.deepEqual(getAwaitArgGuardPayload(laag), { + argGuard: 88, }); t.deepEqual(getMethodGuardPayload(mg1), { @@ -69,9 +68,12 @@ test('legacy guard tolerance', async t => { returnGuard: M.any(), }); // @ts-expect-error Legacy adaptor can be ill typed - t.throws(() => getMethodGuardPayload(lmg), { - message: - 'methodGuard: copyRecord {"argGuards":[77,{"argGuard":88,"klass":"awaitArg"}],"callKind":"async","klass":"methodGuard","returnGuard":"[match:any]"} - Must be a guard:methodGuard', + t.deepEqual(getMethodGuardPayload(lmg), { + callKind: 'async', + argGuards: [77, aag], + optionalArgGuards: undefined, + restArgGuard: undefined, + returnGuard: M.any(), }); t.deepEqual(getInterfaceGuardPayload(ig1), { @@ -97,9 +99,13 @@ test('legacy guard tolerance', async t => { }, ); // @ts-expect-error Legacy adaptor can be ill typed - t.throws(() => getInterfaceGuardPayload(lig), { - message: - 'interfaceGuard: copyRecord {"intefaceName":"Foo","klass":"Interface","methodGuards":{"lmg":{"argGuards":[77,{"argGuard":88,"klass":"awaitArg"}],"callKind":"async","klass":"methodGuard","returnGuard":"[match:any]"},"mg1":"[guard:methodGuard]","mg2":"[guard:methodGuard]"}} - Must be a guard:interfaceGuard', + t.deepEqual(getInterfaceGuardPayload(lig), { + interfaceName: 'Foo', + methodGuards: { + mg1, + mg2, + lmg: M.callWhen(77, M.await(88)).optional().rest(M.any()).returns(M.any()), + }, }); const { meth } = { @@ -111,30 +117,39 @@ test('legacy guard tolerance', async t => { mg2: meth, }); t.deepEqual(await f1.mg1(77, 88), [77, 88]); - await t.throwsAsync(async () => f1.mg2(77, 88), { - message: - 'In "mg2" method of (foo): arg 1: 88 - Must be: {"argGuard":88,"klass":"awaitArg"}', - }); await t.throwsAsync(async () => f1.mg1(77, laag), { message: 'In "mg1" method of (foo): arg 1: {"argGuard":88,"klass":"awaitArg"} - Must be: 88', }); + await t.throwsAsync(async () => f1.mg2(77, 88), { + message: + 'In "mg2" method of (foo): arg 1: 88 - Must be: {"argGuard":88,"klass":"awaitArg"}', + }); t.deepEqual(await f1.mg2(77, laag), [77, laag]); - t.throws( - () => - makeExo( - 'foo', - // @ts-expect-error Legacy adaptor can be ill typed - lig, - { - mg1: meth, - mg2: meth, - }, - ), + const f2 = makeExo( + 'foo', + // @ts-expect-error Legacy adaptor can be ill typed + lig, { - message: - 'interfaceGuard: copyRecord {"intefaceName":"Foo","klass":"Interface","methodGuards":{"lmg":{"argGuards":[77,{"argGuard":88,"klass":"awaitArg"}],"callKind":"async","klass":"methodGuard","returnGuard":"[match:any]"},"mg1":"[guard:methodGuard]","mg2":"[guard:methodGuard]"}} - Must be a guard:interfaceGuard', + mg1: meth, + mg2: meth, + lmg: meth, }, ); + t.deepEqual(await f2.mg1(77, 88), [77, 88]); + await t.throwsAsync(async () => f2.mg1(77, laag), { + message: + 'In "mg1" method of (foo): arg 1: {"argGuard":88,"klass":"awaitArg"} - Must be: 88', + }); + await t.throwsAsync(async () => f2.mg2(77, 88), { + message: + 'In "mg2" method of (foo): arg 1: 88 - Must be: {"argGuard":88,"klass":"awaitArg"}', + }); + t.deepEqual(await f2.mg2(77, laag), [77, laag]); + t.deepEqual(await f2.lmg(77, 88), [77, 88]); + await t.throwsAsync(async () => f2.lmg(77, laag), { + message: + 'In "lmg" method of (foo): arg 1: {"argGuard":88,"klass":"awaitArg"} - Must be: 88', + }); }); diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index 9ff7b58a3e..4e1b68b0b5 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -17,6 +17,7 @@ import { identChecker } from '@endo/common/ident-checker.js'; import { applyLabelingError } from '@endo/common/apply-labeling-error.js'; import { fromUniqueEntries } from '@endo/common/from-unique-entries.js'; import { listDifference } from '@endo/common/list-difference.js'; +import { objectMap } from '@endo/common/object-map.js'; import { q, b, X, Fail, makeError, annotateError } from '@endo/errors'; import { keyEQ, keyGT, keyGTE, keyLT, keyLTE } from '../keys/compareKeys.js'; @@ -1740,6 +1741,14 @@ const AwaitArgGuardPayloadShape = harden({ const AwaitArgGuardShape = M.kind('guard:awaitArgGuard'); +// TODO manually maintain correspondence with AwaitArgGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyAwaitArgGuardShape = harden({ + klass: 'awaitArg', + argGuard: M.pattern(), +}); + /** * @param {any} specimen * @returns {specimen is import('./types.js').AwaitArgGuard} @@ -1757,14 +1766,6 @@ export const assertAwaitArgGuard = specimen => { }; harden(assertAwaitArgGuard); -const LegacyAwaitArgGuardShape = M.splitRecord( - { - klass: 'awaitArg', - }, - undefined, - AwaitArgGuardPayloadShape, -); - /** * By using this abstraction rather than accessing the properties directly, * we smooth the transition to https://github.com/endojs/endo/pull/1712, @@ -1860,6 +1861,60 @@ const MethodGuardPayloadShape = M.or( const MethodGuardShape = M.kind('guard:methodGuard'); +// TODO manually maintain correspondence with SyncMethodGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacySyncMethodGuardShape = M.splitRecord( + { + klass: 'methodGuard', + callKind: 'sync', + argGuards: SyncValueGuardListShape, + returnGuard: SyncValueGuardShape, + }, + { + optionalArgGuards: SyncValueGuardListShape, + restArgGuard: SyncValueGuardShape, + }, +); + +// TODO manually maintain correspondence with ArgGuardShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyArgGuardShape = M.or( + RawGuardShape, + AwaitArgGuardShape, + LegacyAwaitArgGuardShape, + M.pattern(), +); +// TODO manually maintain correspondence with ArgGuardListShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyArgGuardListShape = M.arrayOf(LegacyArgGuardShape); + +// TODO manually maintain correspondence with AsyncMethodGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyAsyncMethodGuardShape = M.splitRecord( + { + klass: 'methodGuard', + callKind: 'async', + argGuards: LegacyArgGuardListShape, + returnGuard: SyncValueGuardShape, + }, + { + optionalArgGuards: ArgGuardListShape, + restArgGuard: SyncValueGuardShape, + }, +); + +// TODO manually maintain correspondence with MethodGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyMethodGuardShape = M.or( + LegacySyncMethodGuardShape, + LegacyAsyncMethodGuardShape, +); + /** * @param {any} specimen * @returns {asserts specimen is import('./types.js').MethodGuard} @@ -1869,13 +1924,10 @@ export const assertMethodGuard = specimen => { }; harden(assertMethodGuard); -const LegacyMethodGuardShape = M.splitRecord( - { - klass: 'methodGuard', - }, - undefined, - MethodGuardPayloadShape, -); +const adaptLegacyArgGuard = argGuard => + matches(argGuard, LegacyAwaitArgGuardShape) + ? M.await(getAwaitArgGuardPayload(argGuard).argGuard) + : argGuard; /** * By using this abstraction rather than accessing the properties directly, @@ -1891,14 +1943,41 @@ const LegacyMethodGuardShape = M.splitRecord( * @returns {import('./types.js').MethodGuardPayload} */ export const getMethodGuardPayload = methodGuard => { - if (matches(methodGuard, LegacyMethodGuardShape)) { + if (matches(methodGuard, MethodGuardShape)) { + return methodGuard.payload; + } + mustMatch(methodGuard, LegacyMethodGuardShape, 'legacyMethodGuard'); + const { // @ts-expect-error Legacy adaptor can be ill typed - const { klass: _, ...payload } = methodGuard; + klass: _, // @ts-expect-error Legacy adaptor can be ill typed - return harden(payload); + callKind, + // @ts-expect-error Legacy adaptor can be ill typed + returnGuard, + // @ts-expect-error Legacy adaptor can be ill typed + restArgGuard, + } = methodGuard; + let { + // @ts-expect-error Legacy adaptor can be ill typed + argGuards, + // @ts-expect-error Legacy adaptor can be ill typed + optionalArgGuards, + } = methodGuard; + if (callKind === 'async') { + argGuards = argGuards.map(adaptLegacyArgGuard); + optionalArgGuards = + optionalArgGuards && optionalArgGuards.map(adaptLegacyArgGuard); } - assertMethodGuard(methodGuard); - return methodGuard.payload; + const payload = harden({ + callKind, + argGuards, + optionalArgGuards, + restArgGuard, + returnGuard, + }); + // ensure the adaptation succeeded. + mustMatch(payload, MethodGuardPayloadShape, 'internalMethodGuardAdaptor'); + return payload; }; harden(getMethodGuardPayload); @@ -1960,6 +2039,28 @@ const InterfaceGuardPayloadShape = M.splitRecord( const InterfaceGuardShape = M.kind('guard:interfaceGuard'); +// TODO manually maintain correspondence with InterfaceGuardPayloadShape +// because this one needs to be stable and accommodate nested legacy, +// when that's an issue. +const LegacyInterfaceGuardShape = M.splitRecord( + { + klass: 'Interface', + interfaceName: M.string(), + methodGuards: M.recordOf( + M.string(), + M.or(MethodGuardShape, LegacyMethodGuardShape), + ), + }, + { + defaultGuards: M.or(M.undefined(), 'passable', 'raw'), + sloppy: M.boolean(), + // There is no need to accommodate LegacyMethodGuardShape in + // this position, since `symbolMethodGuards happened + // after https://github.com/endojs/endo/pull/1712 + symbolMethodGuards: M.mapOf(M.symbol(), MethodGuardShape), + }, +); + /** * @param {any} specimen * @returns {asserts specimen is import('./types.js').InterfaceGuard} @@ -1969,13 +2070,23 @@ export const assertInterfaceGuard = specimen => { }; harden(assertInterfaceGuard); -const LegacyInterfaceGuardShape = M.splitRecord( - { - klass: 'Interface', - }, - undefined, - InterfaceGuardPayloadShape, -); +const adaptMethodGuard = methodGuard => { + if (matches(methodGuard, LegacyMethodGuardShape)) { + const { + callKind, + argGuards, + optionalArgGuards = [], + restArgGuard = M.any(), + returnGuard, + } = getMethodGuardPayload(methodGuard); + const mCall = callKind === 'sync' ? M.call : M.callWhen; + return mCall(...argGuards) + .optional(...optionalArgGuards) + .rest(restArgGuard) + .returns(returnGuard); + } + return methodGuard; +}; /** * By using this abstraction rather than accessing the properties directly, @@ -1992,14 +2103,25 @@ const LegacyInterfaceGuardShape = M.splitRecord( * @returns {import('./types.js').InterfaceGuardPayload} */ export const getInterfaceGuardPayload = interfaceGuard => { - if (matches(interfaceGuard, LegacyInterfaceGuardShape)) { - // @ts-expect-error Legacy adaptor can be ill typed - const { klass: _, ...payload } = interfaceGuard; - // @ts-expect-error Legacy adaptor can be ill typed - return harden(payload); + if (matches(interfaceGuard, InterfaceGuardShape)) { + return interfaceGuard.payload; } - assertInterfaceGuard(interfaceGuard); - return interfaceGuard.payload; + mustMatch(interfaceGuard, LegacyInterfaceGuardShape, 'legacyInterfaceGuard'); + // @ts-expect-error Legacy adaptor can be ill typed + // eslint-disable-next-line prefer-const + let { klass: _, interfaceName, methodGuards, ...rest } = interfaceGuard; + methodGuards = objectMap(methodGuards, adaptMethodGuard); + const payload = harden({ + interfaceName, + methodGuards, + ...rest, + }); + mustMatch( + payload, + InterfaceGuardPayloadShape, + 'internalInterfaceGuardAdaptor', + ); + return payload; }; harden(getInterfaceGuardPayload);