Skip to content

Commit

Permalink
fix(exo): allow richer behaviorMethods
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Oct 6, 2023
1 parent 877be98 commit 55c660a
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 12 deletions.
27 changes: 18 additions & 9 deletions packages/exo/src/exo-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
getMethodGuardPayload,
getInterfaceGuardPayload,
getCopyMapEntries,
objectMetaAssign,
objectMetaMap,
} from '@endo/patterns';

/** @typedef {import('@endo/patterns').Method} Method */
Expand All @@ -22,7 +24,7 @@ import {

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

/**
* A method guard, for inclusion in an interface guard, that enforces only that
Expand Down Expand Up @@ -261,12 +263,17 @@ export const GET_INTERFACE_GUARD = Symbol.for('getInterfaceGuard');
* @returns {T}
*/
const withGetInterfaceGuardMethod = (behaviorMethods, interfaceGuard) =>
harden({
[GET_INTERFACE_GUARD]() {
return interfaceGuard;
objectMetaAssign(
{
__proto__: getPrototypeOf(behaviorMethods),
},
...behaviorMethods,
});
{
[GET_INTERFACE_GUARD]() {
return interfaceGuard;
},
},
behaviorMethods,
);

/**
* @template {Record<PropertyKey, CallableFunction>} T
Expand All @@ -288,9 +295,11 @@ export const defendPrototype = (
if (hasOwnPropertyOf(behaviorMethods, 'constructor')) {
// By ignoring any method named "constructor", we can use a
// class.prototype as a behaviorMethods.
const { constructor: _, ...methods } = behaviorMethods;
// @ts-expect-error TS misses that hasOwn check makes this safe
behaviorMethods = harden(methods);
behaviorMethods = objectMetaMap(
behaviorMethods,
(desc, key) => key === 'constructor' ? undefined : desc,
getPrototypeOf(behaviorMethods),
);
}
/** @type {Record<PropertyKey, MethodGuard> | undefined} */
let methodGuards;
Expand Down
7 changes: 6 additions & 1 deletion packages/patterns/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ export {

// ////////////////// Temporary, until these find their proper home ////////////

export { listDifference, objectMap } from './src/utils.js';
export {
listDifference,
objectMap,
objectMetaMap,
objectMetaAssign,
} from './src/utils.js';

// eslint-disable-next-line import/export
export * from './src/types.js';
83 changes: 81 additions & 2 deletions packages/patterns/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { isPromise } from '@endo/promise-kit';

/** @typedef {import('@endo/marshal').Checker} Checker */

const { fromEntries, entries } = Object;
const {
fromEntries,
entries,
getOwnPropertyDescriptors,
create,
defineProperties,
} = Object;
const { ownKeys } = Reflect;

const { details: X, quote: q, Fail } = assert;
Expand Down Expand Up @@ -92,8 +98,8 @@ harden(fromUniqueEntries);
* a CopyRecord.
*
* @template {Record<string, any>} O
* @param {O} original
* @template R map result
* @param {O} original
* @param {(value: O[keyof O], key: keyof O) => R} mapFn
* @returns {{ [P in keyof O]: R}}
*/
Expand All @@ -104,6 +110,79 @@ export const objectMap = (original, mapFn) => {
};
harden(objectMap);

/**
* Like `objectMap`, but at the reflective level of property descriptors
* rather than property values.
*
* Except for hardening, the edge case behavior is mostly the opposite of
* the `objectMap` edge cases.
* * No matter how mutable the original object, the returned object is
* hardened.
* * All own properties of the original are mapped, even if symbol-named
* or non-enumerable.
* * If any of the original properties were accessors, the descriptor
* containing the getter and setter are given to `metaMapFn`.
* * The own properties of the returned are according to the descriptors
* returned by `metaMapFn`.
* * The returned object will always be a plain object whose state is
* only these mapped own properties. It will inherit from the third
* argument if provided, defaulting to `Object.prototype` if omitted.
*
* Because a property descriptor is distinct from `undefined`, we bundle
* mapping and filtering together. When the `metaMapFn` returns `undefined`,
* that property is omitted from the result.
*
* @template {Record<PropertyKey, any>} O
* @param {O} original
* @param {(
* desc: TypedPropertyDescriptor<O[keyof O]>,
* key: keyof O
* ) => (PropertyDescriptor | undefined)} metaMapFn
* @param {any} [proto]
* @returns {any}
*/
export const objectMetaMap = (
original,
metaMapFn,
proto = Object.prototype,
) => {
const descs = getOwnPropertyDescriptors(original);
const keys = ownKeys(original);

const descEntries = /** @type {[PropertyKey,PropertyDescriptor][]} */ (
keys
.map(key => [key, metaMapFn(descs[key], key)])
.filter(([_key, optDesc]) => optDesc !== undefined)
);
return harden(create(proto, fromUniqueEntries(descEntries)));
};
harden(objectMetaMap);

/**
* Like `Object.assign` but at the reflective level of property descriptors
* rather than property values.
*
* Unlike `Object.assign`, this includes all own properties, whether enumerable
* or not. An original accessor property is copied by sharing its getter and
* setter, rather than calling the getter to obtain a value. If an original
* property is non-configurable, a property of the same name on a later original
* that would conflict instead causes the call to `objectMetaAssign` to throw an
* error.
*
* Returns the enhanced `target` after hardening.
*
* @param {any} target
* @param {any[]} originals
* @returns {any}
*/
export const objectMetaAssign = (target, ...originals) => {
for (const original of originals) {
defineProperties(target, getOwnPropertyDescriptors(original));
}
return harden(target);
};
harden(objectMetaAssign);

/**
*
* @param {Array<string | symbol>} leftNames
Expand Down

0 comments on commit 55c660a

Please sign in to comment.