Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(exo): instance testing #1925

Merged
merged 3 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"',
});
});