Skip to content

Commit

Permalink
fix: review suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jan 13, 2024
1 parent 431812c commit 748249e
Show file tree
Hide file tree
Showing 41 changed files with 464 additions and 324 deletions.
File renamed without changes.
File renamed without changes.
Empty file added packages/common/README.md
Empty file.
File renamed without changes.
10 changes: 10 additions & 0 deletions packages/common/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export * from './src/apply-labeling-error.js';
export * from './src/from-unique-entries.js';
export * from './src/ident-checker.js';
export * from './src/list-difference.js';
export * from './src/make-array-iterator.js';
export * from './src/make-iterator.js';
export * from './src/object-map.js';
export * from './src/object-meta-assign.js';
export * from './src/object-meta-map.js';
export * from './src/throw-labeled.js';
4 changes: 2 additions & 2 deletions packages/utils/package.json → packages/common/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@endo/utils",
"name": "@endo/common",
"version": "1.0.1",
"private": true,
"description": null,
"keywords": [],
"author": "Endo contributors",
"license": "Apache-2.0",
"homepage": "https://github.com/endojs/endo/tree/master/packages/skel#readme",
"homepage": "https://github.com/endojs/endo/tree/master/packages/common#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/endojs/endo.git"
Expand Down
37 changes: 37 additions & 0 deletions packages/common/src/apply-labeling-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { E } from '@endo/eventual-send';
import { isPromise } from '@endo/promise-kit';
import { throwLabeled } from './throw-labeled.js';

/**
* @template A,R
* @param {(...args: A[]) => R} func
* @param {A[]} args
* @param {string|number} [label]
* @returns {R}
*/
export const applyLabelingError = (func, args, label = undefined) => {
if (label === undefined) {
return func(...args);
}
let result;
try {
result = func(...args);
} catch (err) {
throwLabeled(err, label);
}
if (isPromise(result)) {
// Cannot be at-ts-expect-error because there is no type error locally.
// Rather, a type error only as imported into exo.
// @ts-ignore If result is a rejected promise, this will
// return a promise with a different rejection reason. But this
// confuses TypeScript because it types that case as `Promise<never>`
// which is cool for a promise that will never fulfll.
// But TypeScript doesn't understand that this will only happen
// when `result` was a rejected promise. In only this case `R`
// should already allow `Promise<never>` as a subtype.
return E.when(result, undefined, reason => throwLabeled(reason, label));
} else {
return result;
}
};
harden(applyLabelingError);
30 changes: 30 additions & 0 deletions packages/common/src/from-unique-entries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { fromEntries } = Object;
const { ownKeys } = Reflect;

const { quote: q, Fail } = assert;

/**
* Throws if multiple entries use the same property name. Otherwise acts
* like `Object.fromEntries` but hardens the result.
* Use it to protect from property names computed from user-provided data.
*
* @template K,V
* @param {Iterable<[K,V]>} allEntries
* @returns {{[k: K]: V}}
*/
export const fromUniqueEntries = allEntries => {
const entriesArray = [...allEntries];
const result = harden(fromEntries(entriesArray));
if (ownKeys(result).length === entriesArray.length) {
return result;
}
const names = new Set();
for (const [name, _] of entriesArray) {
if (names.has(name)) {
Fail`collision on property name ${q(name)}: ${entriesArray}`;
}
names.add(name);
}
throw Fail`internal: failed to create object from unique entries`;
};
harden(fromUniqueEntries);
36 changes: 36 additions & 0 deletions packages/common/src/ident-checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// TODO Complete migration of Checker type from @endo/pass-style to @endo/common
// by having @endo/pass-style, and everyone else who needs it, import it from
// @endo/common.
/**
* @callback Checker
* Internal to a useful pattern for writing checking logic
* (a "checkFoo" function) that can be used to implement a predicate
* (an "isFoo" function) or a validator (an "assertFoo" function).
*
* * A predicate ideally only returns `true` or `false` and rarely throws.
* * A validator throws an informative diagnostic when the predicate
* would have returned `false`, and simply returns `undefined` normally
* when the predicate would have returned `true`.
* * The internal checking function that they share is parameterized by a
* `Checker` that determines how to proceed with a failure condition.
* Predicates pass in an identity function as checker. Validators
* pass in `assertChecker` which is a trivial wrapper around `assert`.
*
* See the various uses for good examples.
* @param {boolean} cond
* @param {import('ses').Details} [details]
* @returns {boolean}
*/

/**
* In the `assertFoo`/`isFoo`/`checkFoo` pattern, `checkFoo` has a `check`
* parameter of type `Checker`. `assertFoo` calls `checkFoo` passes
* `assertChecker` as the `check` argument. `isFoo` passes `identChecker`
* as the `check` argument. `identChecker` acts precisely like an
* identity function, but is typed as a `Checker` to indicate its
* intended use.
*
* @type {Checker}
*/
export const identChecker = (cond, _details) => cond;
harden(identChecker);
10 changes: 10 additions & 0 deletions packages/common/src/list-difference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
*
* @param {Array<string | symbol>} leftNames
* @param {Array<string | symbol>} rightNames
*/
export const listDifference = (leftNames, rightNames) => {
const rightSet = new Set(rightNames);
return leftNames.filter(name => !rightSet.has(name));
};
harden(listDifference);
25 changes: 25 additions & 0 deletions packages/common/src/make-array-iterator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { makeIterator } from './make-iterator.js';

/**
* A `harden`ing analog of Array.prototype[Symbol.iterator].
*
* @template [T=unknown]
* @param {Array<T>} arr
* @returns {IterableIterator<T>}
*/
export const makeArrayIterator = arr => {
const { length } = arr;
let i = 0;
return makeIterator(() => {
/** @type {T} */
let value;
if (i < length) {
value = arr[i];
i += 1;
return harden({ done: false, value });
}
// @ts-expect-error The terminal value doesn't matter
return harden({ done: true, value });
});
};
harden(makeArrayIterator);
15 changes: 15 additions & 0 deletions packages/common/src/make-iterator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Makes a one-shot iterable iterator from a provided `next` function.
*
* @template [T=unknown]
* @param {() => IteratorResult<T>} next
* @returns {IterableIterator<T>}
*/
export const makeIterator = next => {
const iter = harden({
[Symbol.iterator]: () => iter,
next,
});
return iter;
};
harden(makeIterator);
49 changes: 49 additions & 0 deletions packages/common/src/object-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { entries, fromEntries } = Object;

/**
* By analogy with how `Array.prototype.map` will map the elements of
* an array to transformed elements of an array of the same shape,
* `objectMap` will do likewise for the string-named own enumerable
* properties of an object.
*
* Typical usage applies `objectMap` to a CopyRecord, i.e.,
* an object for which `passStyleOf(original) === 'copyRecord'`. For these,
* none of the following edge cases arise. The result will be a CopyRecord
* with exactly the same property names, whose values are the mapped form of
* the original's values.
*
* When the original is not a CopyRecord, some edge cases to be aware of
* * No matter how mutable the original object, the returned object is
* hardened.
* * Only the string-named enumerable own properties of the original
* are mapped. All other properties are ignored.
* * If any of the original properties were accessors, `Object.entries`
* will cause its `getter` to be called and will use the resulting
* value.
* * No matter whether the original property was an accessor, writable,
* or configurable, all the properties of the returned object will be
* non-writable, non-configurable, data properties.
* * No matter what the original object may have inherited from, and
* no matter whether it was a special kind of object such as an array,
* the returned object will always be a plain object inheriting directly
* from `Object.prototype` and whose state is only these new mapped
* own properties.
*
* With these differences, even if the original object was not a CopyRecord,
* if all the mapped values are Passable, then the returned object will be
* a CopyRecord.
*
* @template {Record<string, any>} O
* @template R map result
* @param {O} original
* @param {(value: O[keyof O], key: keyof O) => R} mapFn
* @returns {Record<keyof O, R>}
*/
export const objectMap = (original, mapFn) => {
const ents = entries(original);
const mapEnts = ents.map(
([k, v]) => /** @type {[keyof O, R]} */ ([k, mapFn(v, k)]),
);
return /** @type {Record<keyof O, R>} */ (harden(fromEntries(mapEnts)));
};
harden(objectMap);
26 changes: 26 additions & 0 deletions packages/common/src/object-meta-assign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { getOwnPropertyDescriptors, defineProperties } = Object;

/**
* 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);
50 changes: 50 additions & 0 deletions packages/common/src/object-meta-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const { getOwnPropertyDescriptors, create, fromEntries } = Object;
const { ownKeys } = Reflect;

/**
* 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, fromEntries(descEntries)));
};
harden(objectMetaMap);
20 changes: 20 additions & 0 deletions packages/common/src/throw-labeled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const { details: X } = assert;

/**
* @param {Error} innerErr
* @param {string|number} label
* @param {ErrorConstructor=} ErrorConstructor
* @returns {never}
*/
export const throwLabeled = (innerErr, label, ErrorConstructor = undefined) => {
if (typeof label === 'number') {
label = `[${label}]`;
}
const outerErr = assert.error(
`${label}: ${innerErr.message}`,
ErrorConstructor,
);
assert.note(outerErr, X`Caused by ${innerErr}`);
throw outerErr;
};
harden(throwLabeled);
File renamed without changes.
28 changes: 28 additions & 0 deletions packages/common/test/test-apply-labeling-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { test } from './prepare-test-env-ava.js';
import { applyLabelingError } from '../src/apply-labeling-error.js';

const { Fail } = assert;

test('test applyLabelingError', async t => {
t.is(
applyLabelingError(x => x * 2, [8]),
16,
);
t.is(
applyLabelingError(x => x * 2, [8], 'foo'),
16,
);
t.is(await applyLabelingError(async x => x * 2, [8], 'foo'), 16);
t.throws(() => applyLabelingError(x => Fail`${x}`, ['e']), {
message: '"e"',
});
t.throws(() => applyLabelingError(x => Fail`${x}`, ['e'], 'foo'), {
message: 'foo: "e"',
});
await t.throwsAsync(
async () => applyLabelingError(x => Fail`${x}`, ['e'], 'foo'),
{
message: 'foo: "e"',
},
);
});
23 changes: 23 additions & 0 deletions packages/common/test/test-from-unique-entries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { test } from './prepare-test-env-ava.js';
import { fromUniqueEntries } from '../src/from-unique-entries.js';

test('test fromUniqueEntries', async t => {
t.deepEqual(
fromUniqueEntries([
['a', 1],
['b', 2],
]),
{ a: 1, b: 2 },
);

t.throws(
() =>
fromUniqueEntries([
['a', 1],
['a', 2],
]),
{
message: 'collision on property name "a": [["a",1],["a",2]]',
},
);
});
7 changes: 7 additions & 0 deletions packages/common/test/test-ident-checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test } from './prepare-test-env-ava.js';
import { identChecker } from '../src/ident-checker.js';

test('test identChecker', async t => {
t.is(identChecker(true, 'x'), true);
t.is(identChecker(false, 'x'), false);
});
Loading

0 comments on commit 748249e

Please sign in to comment.