Skip to content

feat(pass-style): add selector #2774

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

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 2 additions & 0 deletions packages/pass-style/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export {
} from './src/passStyleOf.js';

export { makeTagged } from './src/makeTagged.js';
export { makeSelector } from './src/makeSelector.js';

export {
Remotable,
Far,
Expand Down
27 changes: 27 additions & 0 deletions packages/pass-style/src/makeSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// <reference types="ses"/>

import { Fail } from '@endo/errors';
import { PASS_STYLE } from './passStyle-helpers.js';

/**
* @import {CopySelector} from './types.js'
*/

const { create, prototype: objectPrototype } = Object;

/**
* @template {string} T
* @param {T} tag
* @returns {CopySelector<T>}
*/
export const makeSelector = tag => {
typeof tag === 'string' ||
Fail`The tag of a selector record must be a string: ${tag}`;
return harden(
create(objectPrototype, {
[PASS_STYLE]: { value: 'selector' },
[Symbol.toStringTag]: { value: tag },
}),
);
};
harden(makeSelector);
52 changes: 52 additions & 0 deletions packages/pass-style/src/passStyle-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,55 @@ export const checkFunctionTagRecord = makeCheckTagRecord(
)),
);
harden(checkFunctionTagRecord);

/**
* @param {import('./types.js').PassStyled<any, any>} selectorRecord
* @param {PassStyle} expectedPassStyle
* @param {Checker} [check]
* @returns {boolean}
*/
export const checkSelectorRecord = (
selectorRecord,
expectedPassStyle,
check,
) => {
const checkProto = (val, proto) =>
proto === objectPrototype ||
(!!check &&
check(
false,
X`A selectorRecord must inherit from Object.prototype: ${val}`,
));

return (
(isObject(selectorRecord) ||
(!!check &&
CX(
check,
)`A non-object cannot be a selectorRecord: ${selectorRecord}`)) &&
(isFrozen(selectorRecord) ||
(!!check &&
CX(check)`A selectorRecord must be frozen: ${selectorRecord}`)) &&
(!isArray(selectorRecord) ||
(!!check &&
CX(check)`An array cannot be a selectorRecord: ${selectorRecord}`)) &&
checkPassStyle(
selectorRecord,
getOwnDataDescriptor(selectorRecord, PASS_STYLE, false, check).value,
expectedPassStyle,
check,
) &&
(typeof getOwnDataDescriptor(
selectorRecord,
Symbol.toStringTag,
false,
check,
).value === 'string' ||
(!!check &&
CX(
check,
)`A [Symbol.toStringTag]-named property must be a string: ${selectorRecord}`)) &&
checkProto(selectorRecord, getPrototypeOf(selectorRecord))
);
};
harden(checkSelectorRecord);
3 changes: 3 additions & 0 deletions packages/pass-style/src/passStyleOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { RemotableHelper } from './remotable.js';
import { assertPassableSymbol } from './symbol.js';
import { assertSafePromise } from './safe-promise.js';
import { assertPassableString } from './string.js';
import { SelectorHelper } from './selector.js';

/** @import {PassStyleHelper} from './internal-types.js' */
/** @import {CopyArray, CopyRecord, CopyTagged, Passable} from './types.js' */
Expand All @@ -50,6 +51,7 @@ const makeHelperTable = passStyleHelpers => {
copyArray: undefined,
copyRecord: undefined,
tagged: undefined,
selector: undefined,
error: undefined,
remotable: undefined,
};
Expand Down Expand Up @@ -238,6 +240,7 @@ export const passStyleOf =
CopyArrayHelper,
CopyRecordHelper,
TaggedHelper,
SelectorHelper,
ErrorHelper,
RemotableHelper,
]);
Expand Down
43 changes: 43 additions & 0 deletions packages/pass-style/src/selector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/// <reference types="ses"/>

import { Fail } from '@endo/errors';
import {
assertChecker,
checkSelectorRecord,
PASS_STYLE,
checkPassStyle,
} from './passStyle-helpers.js';

/**
* @import {PassStyleHelper} from './internal-types.js'
*/

const { ownKeys } = Reflect;
const { getOwnPropertyDescriptors } = Object;

/**
*
* @type {PassStyleHelper}
*/
export const SelectorHelper = harden({
styleName: 'selector',

canBeValid: (candidate, check = undefined) =>
checkPassStyle(candidate, candidate[PASS_STYLE], 'selector', check),

assertRestValid: (candidate, passStyleOfRecur) => {
checkSelectorRecord(candidate, 'selector', assertChecker);

// Typecasts needed due to https://github.com/microsoft/TypeScript/issues/1863
const passStyleKey = /** @type {unknown} */ (PASS_STYLE);
const tagKey = /** @type {unknown} */ (Symbol.toStringTag);
const {
// checkTagRecord already verified PASS_STYLE and Symbol.toStringTag own data properties.
[/** @type {string} */ (passStyleKey)]: _passStyleDesc,
[/** @type {string} */ (tagKey)]: _labelDesc,
...restDescs
} = getOwnPropertyDescriptors(candidate);
ownKeys(restDescs).length === 0 ||
Fail`Unexpected properties on selector record ${ownKeys(restDescs)}`;
},
});
16 changes: 14 additions & 2 deletions packages/pass-style/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type PrimitiveStyle =
| 'string'
| 'symbol';

export type ContainerStyle = 'copyRecord' | 'copyArray' | 'tagged';
export type ContainerStyle = 'copyRecord' | 'copyArray' | 'tagged' | 'selector';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t believe selector qualifies as a container. It’s more like a string. But, the reïfication of a selector is an object, so that might be the distinguishing feature for containers. I do not know.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its a container for a string! yeah ok was mostly just following 'tagged' around the code base

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely not a container. Our best documentation is at https://endojs.github.io/endo/types/_endo_pass_style.Passable.html , and we could describe Selector as primitive if we're taking a data model perspective, or otherwise a new special case (because, unlike the existing primitives, Object.is will differentiate identical values).


export type PassStyle =
| PrimitiveStyle
Expand All @@ -31,7 +31,7 @@ export type PassStyle =
| 'error'
| 'promise';

export type TaggedOrRemotable = 'tagged' | 'remotable';
export type TaggedOrRemotable = 'tagged' | 'selector' | 'remotable';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selector definitely does not belong in this union.


/**
* Tagged has own [PASS_STYLE]: "tagged", [Symbol.toStringTag]: $tag.
Expand Down Expand Up @@ -174,6 +174,18 @@ export type CopyTagged<
> = PassStyled<'tagged', Tag> & {
payload: Payload;
};

/**
* A Passable "selector record".
* It must have a property with key equal to the `PASS_STYLE` export and
* value 'selector'
* and no other properties except `[Symbol.toStringTag]`.
*/
export type CopySelector<Tag extends string = string> = PassStyled<
'selector',
Tag
>;

/**
* This is an interface specification.
* For now, it is just a string, but we retain the option to make it `PureData`.
Expand Down
68 changes: 68 additions & 0 deletions packages/pass-style/test/passStyleOf.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,74 @@ test('passStyleOf testing tagged records', t => {
}
});

test('passStyleOf testing selector records', t => {
const makeSelectorRecordVariant = (tag, proto) => {
const record = Object.create(
proto === undefined ? Object.prototype : proto,
{
[PASS_STYLE]: { value: 'selector' },
[Symbol.toStringTag]: {
value: tag,
configurable: true,
writable: true,
},
},
);
return record;
};
t.is(passStyleOf(harden(makeSelectorRecordVariant('foo'))), 'selector');
t.is(passStyleOf(harden(makeSelectorRecordVariant(''))), 'selector');

for (const proto of [null, harden({})]) {
const selectorRecordBadProto = makeSelectorRecordVariant('bar', proto);
t.throws(
() => passStyleOf(harden(selectorRecordBadProto)),
{ message: /A selectorRecord must inherit from Object.prototype/ },
`quasi-selectorRecord with ${proto} prototype`,
);
}

const selectorRecordExtra = makeSelectorRecordVariant('baz');
Object.defineProperty(selectorRecordExtra, 'extra', {
value: 'unexpected own property',
});
t.throws(() => passStyleOf(harden(selectorRecordExtra)), {
message: 'Unexpected properties on selector record ["extra"]',
});

const selectorRecordBadTags = [
{
label: 'absent',
message: '"[Symbol(Symbol.toStringTag)]" property expected: {}',
},
{
label: 'invalid-tag-number',
value: 0,
message: 'A [Symbol.toStringTag]-named property must be a string: "[0]"',
},
{
label: 'invalid-tag-symbol',
value: Symbol('invalid'),
message:
'A [Symbol.toStringTag]-named property must be a string: [Something that failed to stringify]',
},
];
for (const testCase of selectorRecordBadTags) {
const { label, message, ...desc } = testCase;
const selectorRecordBadTag = makeSelectorRecordVariant('foo');
if (ownKeys(desc).length === 0) {
Reflect.deleteProperty(selectorRecordBadTag, Symbol.toStringTag);
} else {
Reflect.defineProperty(selectorRecordBadTag, Symbol.toStringTag, desc);
}
t.throws(
() => passStyleOf(harden(selectorRecordBadTag)),
{ message },
`selector record with tag ${label} must be rejected`,
);
}
});

test('passStyleOf testing remotables', t => {
t.is(passStyleOf(Far('foo', {})), 'remotable');
t.is(passStyleOf(Far('foo', () => 'far function')), 'remotable');
Expand Down
Loading