Skip to content

Commit

Permalink
feat(exo): instance testing (#1925)
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jan 19, 2024
1 parent 88c659b commit 05d46d6
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 9 deletions.
75 changes: 67 additions & 8 deletions packages/exo/src/exo-makers.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const initEmpty = () => emptyRecord;
*/

/**
* The power to amplify a live facet instance of the associated exo class kit
* The power to amplify a facet instance of the associated exo class kit
* into the record of all facets of this facet instance's cohort.
*
* @template {any} [F=any]
Expand All @@ -97,6 +97,19 @@ export const initEmpty = () => emptyRecord;
* @returns {F}
*/

/**
* The power to test if a value is an instance of the
* associated exo class, or a facet instance of the
* associated exo class kit. In the later case, if a `facetName` is provided,
* then it tests only whether the argument is a facet instance of that
* facet of the associated exo class kit.
*
* @callback IsInstance
* @param {any} exo
* @param {string} [facetName]
* @returns {boolean}
*/

// TODO Should we split FarClassOptions into distinct types for
// class options vs class kit options? After all, `receiveAmplifier`
// makes no sense for normal exo classes.
Expand Down Expand Up @@ -129,9 +142,18 @@ export const initEmpty = () => emptyRecord;
* definition of the exo class kit with an `Amplify` function. If called
* during the definition of a normal exo or exo class, it will throw, since
* only exo kits can be amplified.
* An `Amplify` function is a function that takes a live facet instance of
* An `Amplify` function is a function that takes a facet instance of
* this class kit as an argument, in which case it will return the facets
* record, giving access to all the facet instances of the same cohort.
*
* @property {ReceivePower<IsInstance>} [receiveInstanceTester]
* If a `receiveInstanceTester` function is provided, it will be called
* during the definition of the exo class or exo class kit with an
* `IsInstance` function. The first argument of `IsInstance`
* is the value to be tested. When it may be a facet instance of an
* exo class kit, the optional second argument, if provided, is
* a `facetName`. In that case, the function tests only if the first
* argument is an instance of that facet of the associated exo class kit.
*/

/**
Expand Down Expand Up @@ -169,7 +191,11 @@ export const defineExoClass = (
options = {},
) => {
harden(methods);
const { finish = undefined, receiveAmplifier = undefined } = options;
const {
finish = undefined,
receiveAmplifier = undefined,
receiveInstanceTester = undefined,
} = options;
receiveAmplifier === undefined ||
Fail`Only facets of an exo class kit can be amplified ${q(tag)}`;

Expand Down Expand Up @@ -202,6 +228,18 @@ export const defineExoClass = (
return self;
};

if (receiveInstanceTester) {
const isInstance = (exo, facetName = undefined) => {
facetName === undefined ||
Fail`facetName can only be used with an exo class kit: ${q(
tag,
)} has no facet ${q(facetName)}`;
return contextMap.has(exo);
};
harden(isInstance);
receiveInstanceTester(isInstance);
}

return harden(makeInstance);
};
harden(defineExoClass);
Expand Down Expand Up @@ -229,7 +267,11 @@ export const defineExoClassKit = (
options = {},
) => {
harden(methodsKit);
const { finish = undefined, receiveAmplifier = undefined } = options;
const {
finish = undefined,
receiveAmplifier = undefined,
receiveInstanceTester = undefined,
} = options;
const contextMapKit = objectMap(methodsKit, () => new WeakMap());
const getContextKit = objectMap(
contextMapKit,
Expand Down Expand Up @@ -268,19 +310,36 @@ export const defineExoClassKit = (
};

if (receiveAmplifier) {
const amplify = aFacet => {
const amplify = exoFacet => {
for (const contextMap of values(contextMapKit)) {
if (contextMap.has(aFacet)) {
const { facets } = contextMap.get(aFacet);
if (contextMap.has(exoFacet)) {
const { facets } = contextMap.get(exoFacet);
return facets;
}
}
throw Fail`Must be a facet of ${q(tag)}: ${aFacet}`;
throw Fail`Must be a facet of ${q(tag)}: ${exoFacet}`;
};
harden(amplify);
receiveAmplifier(amplify);
}

if (receiveInstanceTester) {
const isInstance = (exoFacet, facetName = undefined) => {
if (facetName === undefined) {
return values(contextMapKit).some(contextMap =>
contextMap.has(exoFacet),
);
}
assert.typeof(facetName, 'string');
const contextMap = contextMapKit[facetName];
contextMap !== undefined ||
Fail`exo class kit ${q(tag)} has no facet named ${q(facetName)}`;
return contextMap.has(exoFacet);
};
harden(isInstance);
receiveInstanceTester(isInstance);
}

return harden(makeInstanceKit);
};
harden(defineExoClassKit);
Expand Down
2 changes: 1 addition & 1 deletion packages/exo/test/test-amplify-heap-class-kits.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// modeled on test-revoke-heap-classes.js
// modeled on test-heap-classes.js

// eslint-disable-next-line import/order
import { test } from './prepare-test-env-ava.js';
Expand Down
97 changes: 97 additions & 0 deletions packages/exo/test/test-is-instance-heap-class-kits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// modeled on test-heap-classes.js

// eslint-disable-next-line import/order
import { test } from './prepare-test-env-ava.js';

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

const UpCounterI = M.interface('UpCounter', {
incr: M.call().optional(M.gte(0)).returns(M.number()),
});

const DownCounterI = M.interface('DownCounter', {
decr: M.call().optional(M.gte(0)).returns(M.number()),
});

test('test isInstance defineExoClass', t => {
let isInstance;
const makeUpCounter = defineExoClass(
'UpCounter',
UpCounterI,
/** @param {number} x */
(x = 0) => ({ x }),
{
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
{
receiveInstanceTester(i) {
isInstance = i;
},
},
);
t.is(isInstance(harden({})), false);
t.throws(() => isInstance(harden({}), 'up'), {
message:
'facetName can only be used with an exo class kit: "UpCounter" has no facet "up"',
});

const upCounter = makeUpCounter(3);

t.is(isInstance(upCounter), true);
t.throws(() => isInstance(upCounter, 'up'), {
message:
'facetName can only be used with an exo class kit: "UpCounter" has no facet "up"',
});
});

test('test isInstance defineExoClassKit', t => {
let isInstance;
const makeCounterKit = defineExoClassKit(
'Counter',
{ up: UpCounterI, down: DownCounterI },
/** @param {number} x */
(x = 0) => ({ x }),
{
up: {
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
down: {
decr(y = 1) {
const { state } = this;
state.x -= y;
return state.x;
},
},
},
{
receiveInstanceTester(i) {
isInstance = i;
},
},
);

t.is(isInstance(harden({})), false);
t.is(isInstance(harden({}), 'up'), false);
t.throws(() => isInstance(harden({}), 'foo'), {
message: 'exo class kit "Counter" has no facet named "foo"',
});

const { up: upCounter } = makeCounterKit(3);

t.is(isInstance(upCounter), true);
t.is(isInstance(upCounter, 'up'), true);
t.is(isInstance(upCounter, 'down'), false);
t.throws(() => isInstance(upCounter, 'foo'), {
message: 'exo class kit "Counter" has no facet named "foo"',
});
});

0 comments on commit 05d46d6

Please sign in to comment.