Skip to content

Commit

Permalink
feat(pass-style): generalize passable errors, throwables
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Apr 21, 2024
1 parent 7c77eff commit dd4b963
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 82 deletions.
151 changes: 73 additions & 78 deletions packages/pass-style/src/error.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/// <reference types="ses"/>

import { X, q } from '@endo/errors';
import { assertChecker } from './passStyle-helpers.js';
import { assertChecker, isObject } from './passStyle-helpers.js';

/** @import {PassStyleHelper} from './internal-types.js' */
/** @import {Checker, PassStyleOf} from './types.js' */
/** @import {Checker, PassStyleOf, CopyTagged, Passable} from './types.js' */

const { getPrototypeOf, getOwnPropertyDescriptors, hasOwn, entries } = Object;
const { getPrototypeOf, getOwnPropertyDescriptors, hasOwn, entries, values } =
Object;

// TODO: Maintenance hazard: Coordinate with the list of errors in the SES
// whilelist.
Expand Down Expand Up @@ -62,7 +63,6 @@ const checkErrorLike = (candidate, check = undefined) => {
);
};
harden(checkErrorLike);
/// <reference types="ses"/>

/**
* Validating error objects are passable raises a tension between security
Expand All @@ -86,27 +86,22 @@ export const isErrorLike = candidate => checkErrorLike(candidate);
harden(isErrorLike);

/**
* An own property of a passable error must be a data property whose value is
* a throwable value.
*
* @param {string} propName
* @param {PropertyDescriptor} desc
* @param {PassStyleOf} passStyleOfRecur
* @param {Checker} [check]
* @returns {boolean}
*/
export const checkRecursivelyPassableErrorPropertyDesc = (
export const checkRecursivelyPassableErrorOwnPropertyDesc = (
propName,
desc,
passStyleOfRecur,
check = undefined,
) => {
const reject = !!check && ((T, ...subs) => check(false, X(T, ...subs)));
if (desc.enumerable) {
return (
reject &&
reject`Passable Error ${q(
propName,
)} own property must not be enumerable: ${desc}`
);
}
if (!hasOwn(desc, 'value')) {
return (
reject &&
Expand All @@ -116,89 +111,88 @@ export const checkRecursivelyPassableErrorPropertyDesc = (
);
}
const { value } = desc;
switch (propName) {
case 'message':
case 'stack': {
return (
typeof value === 'string' ||
(reject &&
reject`Passable Error ${q(
propName,
)} own property must be a string: ${value}`)
);
}
case 'cause': {
// eslint-disable-next-line no-use-before-define
return checkRecursivelyPassableError(value, passStyleOfRecur, check);
}
case 'errors': {
if (!Array.isArray(value) || passStyleOfRecur(value) !== 'copyArray') {
return (
reject &&
reject`Passable Error ${q(
propName,
)} own property must be a copyArray: ${value}`
);
}
return value.every(err =>
// eslint-disable-next-line no-use-before-define
checkRecursivelyPassableError(err, passStyleOfRecur, check),
);
}
default: {
break;
}
}
return (
reject && reject`Passable Error has extra unpassed property ${q(propName)}`
);
// eslint-disable-next-line no-use-before-define
return checkRecursivelyThrowable(value, passStyleOfRecur, check);
};
harden(checkRecursivelyPassableErrorPropertyDesc);
harden(checkRecursivelyPassableErrorOwnPropertyDesc);

/**
* `candidate` is throwable if it contains only data and passable errors.
*
* @param {unknown} candidate
* @param {PassStyleOf} passStyleOfRecur
* @param {Checker} [check]
* @returns {boolean}
*/
export const checkRecursivelyPassableError = (
export const checkRecursivelyThrowable = (
candidate,
passStyleOfRecur,
check = undefined,
) => {
const reject = !!check && ((T, ...subs) => check(false, X(T, ...subs)));
if (!checkErrorLike(candidate, check)) {
return false;
}
const proto = getPrototypeOf(candidate);
const { name } = proto;
const errConstructor = getErrorConstructor(name);
if (errConstructor === undefined || errConstructor.prototype !== proto) {
return (
reject &&
reject`Passable Error must inherit from an error class .prototype: ${candidate}`
if (checkErrorLike(candidate, check)) {
const proto = getPrototypeOf(candidate);
const { name } = proto;
const errConstructor = getErrorConstructor(name);
if (errConstructor === undefined || errConstructor.prototype !== proto) {
return (
reject &&
reject`Passable Error must inherit from an error class .prototype: ${candidate}`
);
}
const descs = getOwnPropertyDescriptors(candidate);
if (!('message' in descs)) {
return (
reject &&
reject`Passable Error must have an own "message" string property: ${candidate}`
);
}

return entries(descs).every(([propName, desc]) =>
checkRecursivelyPassableErrorOwnPropertyDesc(
propName,
desc,
passStyleOfRecur,
check,
),
);
}
const descs = getOwnPropertyDescriptors(candidate);
if (!('message' in descs)) {
return (
reject &&
reject`Passable Error must have an own "message" string property: ${candidate}`
);
const passStyle = passStyleOfRecur(candidate);
if (!isObject(candidate)) {
// All passable primitives are throwable
return true;
}
switch (passStyle) {
case 'copyArray': {
return /** @type {Passable[]} */ (candidate).every(element =>
checkRecursivelyThrowable(element, passStyleOfRecur, check),
);
}
case 'copyRecord': {
return values(/** @type {Record<string,any>} */ (candidate)).every(
value => checkRecursivelyThrowable(value, passStyleOfRecur, check),
);
}
case 'tagged': {
return checkRecursivelyThrowable(
/** @type {CopyTagged} */ (candidate).payload,
passStyleOfRecur,
check,
);
}
default: {
return (
reject &&
reject`A throwable cannot contain a ${q(passStyle)}: ${candidate}`
);
}
}

return entries(descs).every(([propName, desc]) =>
checkRecursivelyPassableErrorPropertyDesc(
propName,
desc,
passStyleOfRecur,
check,
),
);
};
harden(checkRecursivelyPassableError);
harden(checkRecursivelyThrowable);

/**
* A passable error is a throwable error and contains only throwable values.
*
* @type {PassStyleHelper}
*/
export const ErrorHelper = harden({
Expand All @@ -207,5 +201,6 @@ export const ErrorHelper = harden({
canBeValid: checkErrorLike,

assertValid: (candidate, passStyleOfRecur) =>
checkRecursivelyPassableError(candidate, passStyleOfRecur, assertChecker),
checkErrorLike(candidate, assertChecker) &&
checkRecursivelyThrowable(candidate, passStyleOfRecur, assertChecker),
});
16 changes: 12 additions & 4 deletions packages/pass-style/src/passStyleOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { CopyRecordHelper } from './copyRecord.js';
import { TaggedHelper } from './tagged.js';
import {
ErrorHelper,
checkRecursivelyPassableErrorPropertyDesc,
checkRecursivelyPassableError,
checkRecursivelyPassableErrorOwnPropertyDesc,
checkRecursivelyThrowable,
getErrorConstructor,
} from './error.js';
import { RemotableHelper } from './remotable.js';
Expand Down Expand Up @@ -264,7 +264,7 @@ harden(isPassable);
* @returns {boolean}
*/
const isPassableErrorPropertyDesc = (name, desc) =>
checkRecursivelyPassableErrorPropertyDesc(name, desc, passStyleOf);
checkRecursivelyPassableErrorOwnPropertyDesc(name, desc, passStyleOf);

/**
* Return a passable error that propagates the diagnostic info of the
Expand All @@ -277,7 +277,7 @@ const isPassableErrorPropertyDesc = (name, desc) =>
*/
export const toPassableError = err => {
harden(err);
if (checkRecursivelyPassableError(err, passStyleOf)) {
if (checkRecursivelyThrowable(err, passStyleOf)) {
return err;
}
const { name, message } = err;
Expand Down Expand Up @@ -309,3 +309,11 @@ export const toPassableError = err => {
return newError;
};
harden(toPassableError);

export const toThrowable = candidate => {
harden(candidate);
if (ErrorHelper.canBeValid(candidate)) {
return toPassableError(candidate);
}
throw Fail`TODO oops`;
}

0 comments on commit dd4b963

Please sign in to comment.