Skip to content

Commit

Permalink
Added meta properties
Browse files Browse the repository at this point in the history
  • Loading branch information
BadIdeaException committed Feb 18, 2024
1 parent 26e8e2a commit 3409d3d
Show file tree
Hide file tree
Showing 6 changed files with 716 additions and 156 deletions.
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Note that ambiguity is a property of the selector itself, independent the result

The empty string `''` is called the **empty** selector. By definition, it selects the input object.

_Note: This behavior is deprecated and now discouraged. It may be changed in the future. Use [::root](#pseudo-elements) instead._
*Note: This behavior is deprecated and now discouraged. It may be changed in the future. Use [::root](#pseudo-elements) instead.*

## Notation

Expand Down Expand Up @@ -130,6 +130,26 @@ Pseudo property | Meaning | Example
`::first` | Selects the first element of an array, the first property of an object, or the first character of a string. Selects nothing on anything else. | `arr.::first` selects the first element of array `arr`. (Same as `arr.0`)
`::last` | Selects the last element of an array, the last property of an object, or the last character of a string. Selects nothing on anthing else. | `str.::last` selects the last character of string `str`

### Meta properties

Meta properties start with a single colon `:`. Similar to conditions, they constrain the selected properties based on their characteristics. Thus, they work as filters. They resemble CSS pseudo classes, and follow the same syntax.

More than one meta property may be used. In this case, they will be applied in the order they are given.

Meta properties, unlike pseudo properties, must not be preceded by a `.`.

Meta property | Meaning | Example
:--- | :--- | :---
`:string`, `:number`, `:bigint`, `:boolean`, `:symbol` | Restricts the selected properties to ones matching the associated type | Given `obj = { a: true, b: 1 }`, `*:boolean` selects `obj.a`, but not `obj.b`.
`:null`, `:undefined` | Restricts the selected properties to ones that are `null`/`undefined`, resp. | Given `obj = { a: null, b: 1 }`, `*:null` selects `obj.a`, but not `obj.b`.
`:object` | Restricts the selected properties to objects, excluding arrays. (To include arrays, use `:complex`.) | Given `obj = { a: {}, b: [] }`, `*:object` selects `obj.a`, but not `obj.b`.
`:array` | Restricts the selected properties to arrays (as per `Array.isArray()`). | Given `obj = { a: {}, b: [] }`, `*:array` selects `obj.b`, but not `obj.a`.
`:primitive` | Restricts the selected properties to [primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive).
`:complex` | Restricts the selected properties to objects and arrays. | Given `obj = { a: {}, b: [] }`, `*:complex` selects `obj.a` and `obj.b`.
`:existent` | Restricts the selected properties such that `null` and `undefined` values are filtered out. | Given `obj = { a: null, b: 1 }`, `*:existent` selects `obj.b`, but not `obj.a`.
`:nonexistent` | Restricts the selected properties to `null` and `undefined` values. | Given `obj = { a: null, b: 1 }`, `*:nonexistent` selects `obj.a`, but not `obj.b`.
`:unique` | Restricts the selected properties such that only the first occurence of each value is kept. | Given `obj = { a: 1, b: 1 }`, `*:unique` selects `obj.a`, but not `obj.b`.

## Examples

const obj = {
Expand Down Expand Up @@ -199,7 +219,8 @@ If it is unambiguous, the result is returned as a scalar. `options.collate` can
* Setting `options.collate` to `false` will *always* return an array, even if there is only one result.
* Setting `options.collate` to `true` will check that all results are deeply equal, and if they are, return their value as a scalar.
If the results are not all deeply equal, an error will be thrown. (Note that the function will still have been applied, though.)
* Setting `options.collate` to a function value will check that all results are equal by using the function for pairwise comparison.
* Setting `options.collate` to a function value will check that after applying that function to all results they are all equal. Note that
the function is only used for determining collation equality -- the returned results are still the same.

*Note: In versions prior 2.0, this function was called `apply`. This has been changed to `perform` to avoid a name conflict with
`Function.prototype.apply` in compiled selectors.*
Expand All @@ -213,17 +234,18 @@ If it is unambiguous, the result is returned as a scalar. `options.collate` can
* `options` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** An optional object with further options for the operation

* `options.collate` **([boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean) | [function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function))?** Whether to collate the results or not. Defaults to `true` on unambiguous selectors, and to `false` on ambiguous ones.
When collating, an error is thrown if the results of applying `fn` to all selected properties are not all deeply equal.
If set to a comparator function, this function is used instead of deep equality.
When collating, an error is thrown if the results of applying `fn` to all selected properties are not all strictly equal in terms of their JSON representation.
If set to a function, this function is applied to all results, then those results are checked for (strict) equality.
Note that this may be quite performance heavy if a lot of properties are selected and/or the comparator is computationally expensive.
* `options.unique` **([boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean) | [function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function))?** Whether to filter out duplicate values before applying `fn`. If set to `true`, strict equality is
used to compare values. Alternatively, can be set to a comparator function which will then be used to determine equality. For duplicate values,
only the first occurence is kept. Note that `options.unique` differs from `options.collate` in that it filters the selection *before* the
function is applied.
Note that this may be quite performance heavy if a lot of properties are selected and/or the comparator is computationally expensive.
*Note: This functionality is deprecated. Use the [:unique meta property](#meta-properties) instead.*
* `options.mode` **(`"normal"` | `"strict"` | `"lenient"`)** The selection mode to use. In `normal` mode, it is permissible to select a non-existent property
as long as it is the terminal portion of the selector. I.e. it is permissible to select `'a'` on `{}`, but not `'a.b'`. This mode
mimics the ordinary rules of selecting object properties in Javascript (where `{}['a'] === undefined`).
mimics the ordinary rules of selecting object properties in Javascript (where `{}.a === undefined`).
In `strict` mode, any attempt to select a non-existent property immediately results in an error.
In `lenient` mode, non-existent properties are silently dropped.
The default mode is `normal`. (optional, default `'normal'`)
Expand Down
4 changes: 3 additions & 1 deletion applicators.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ function _perform(select, fn, obj, options) {
let resolution = select(obj, options?.references, mode);

if (options?.unique) {
console.warn('Using options.unique is deprecated. Use the :unique meta property instead.');
// If options.unique === true, default to strict equality as the comparator
const comp = typeof options.unique === 'function' ? options.unique : (a, b) => a === b;
// Run through the resolutions and for all selections, remove any duplicates, as determined by the comparator function.
//
//
// For this, only check items' selections that come AFTER the current one, e.g. given the following resolutions:
// a b
// 0 1 2 0 1 a0 a1 a2 b0 b2
Expand Down Expand Up @@ -130,6 +131,7 @@ export function compile(selector) {
* only the first occurence is kept. Note that `options.unique` differs from `options.collate` in that it filters the selection _before_ the
* function is applied.
* Note that this may be quite performance heavy if a lot of properties are selected and/or the comparator is computationally expensive.
* _Note: This functionality is deprecated. Use the [:unique meta property](#meta-properties) instead._
* @param {'normal'|'strict'|'lenient'} [options.mode='normal'] The selection mode to use. In `normal` mode, it is permissible to select a non-existent property
* as long as it is the terminal portion of the selector. I.e. it is permissible to select `'a'` on `{}`, but not `'a.b'`. This mode
* mimics the ordinary rules of selecting object properties in Javascript (where `{}['a'] === undefined`).
Expand Down
204 changes: 204 additions & 0 deletions selector-semantics.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,210 @@ describe('Selector semantics', function() {
});
});

describe('Meta properties', function() {
// eslint-disable-next-line mocha/no-setup-in-describe
const PRIMITIVES = [ 'string', 123, 123n, true, undefined, Symbol(), null ]

// eslint-disable-next-line mocha/no-setup-in-describe
describe(`${new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(PRIMITIVES.map(primitive => primitive === null ? 'null' : typeof primitive).map(metaProperty => `:${metaProperty}`))}`, function() {
it('should select primitives of the given type', function() {
PRIMITIVES.forEach(primitive => {
const obj = { a: primitive }

const resolution = parse(`*:${primitive === null ? 'null' : typeof primitive}`)(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'a' ]);
});
});

it('should discard all other values', function() {
PRIMITIVES.forEach(primitive => {
const others = [ {}, [], ...PRIMITIVES.filter(x => x !== primitive) ];

others.forEach(other => {
const obj = { a: other };

const resolution = parse(`*:${primitive === null ? 'null' : typeof primitive}`)(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.is.empty;
});
});
});
});

describe(':primitive', function() {
it(`should select values of type ${new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(PRIMITIVES.map(primitive => primitive === null ? 'null' : typeof primitive))}`, function() {
PRIMITIVES.forEach(primitive => {
const obj = { a: primitive }

const resolution = parse('*:primitive')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'a' ]);
});
});

it('should discard values of type object', function() {
const obj = { a: {} }

const resolution = parse('*:primitive')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.is.empty;
});
});

describe(':object', function() {
it('should select values of type object', function() {
const obj = { a: {} }

const resolution = parse('*:object')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'a' ]);
});

it('should not select primitives', function() {
PRIMITIVES.forEach(primitive => {
const obj = { a: primitive }

const resolution = parse('*:object')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.is.empty;
});
});

it('should not select arrays', function() {
const obj = { a: [] }

const resolution = parse('*:object')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.is.empty;
});
});

describe(':array', function() {
it('should select values of type array', function() {
const obj = { a: [] }

const resolution = parse('*:array')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'a' ]);
});

it('should not select objects or primitives', function() {
[ {}, ...PRIMITIVES ].forEach(objectOrPrimitive => {
const obj = { a: objectOrPrimitive }

const resolution = parse('*:array')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.is.empty;
});
});
});

describe(':complex', function() {
it('should select values of type array', function() {
const obj = { a: [] }

const resolution = parse('*:complex')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'a' ]);
});

it('should select values of type object', function() {
const obj = { a: {} }

const resolution = parse('*:complex')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'a' ]);
});

it('should not select primitives', function() {
PRIMITIVES.forEach(primitive => {
const obj = { a: primitive }

const resolution = parse('*:complex')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.is.empty;
});
});
});

describe(':existent', function() {
it('should select values other than null/undefined', function() {
const obj = { a: null, b: undefined, c: 1 }

const resolution = parse('*:existent')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'c' ]);
});
});

describe(':nonexistent', function() {
it('should select only values null/undefined', function() {
const obj = { a: null, b: undefined, c: 1 }

const resolution = parse('*:nonexistent')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'a', 'b' ]);
});
});

describe(':unique', function() {
it('should filter out values that are present more than once', function() {
const obj = {
a: 1,
b: 1
}

const resolution = parse('*:unique')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'a' ]);
});

it('should not filter out values that are present only once', function() {
const obj = {
a: 1,
b: 2
}

const resolution = parse('*:unique')(obj);

expect(resolution).to.be.an('array').with.lengthOf(1);
expect(resolution[0]).to.have.property('target').that.equals(obj);
expect(resolution[0]).to.have.property('selection').that.has.members([ 'a', 'b' ]);
});
});
});

describe('Conditional selectors', function() {
it('should select with complex selectors in conditions', function() {
const obj = {
Expand Down
16 changes: 16 additions & 0 deletions selector-syntax.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@ describe('Selector syntax', function() {
});
});

describe('Meta properties', function() {
// eslint-disable-next-line mocha/no-setup-in-describe
[
['string', 'number', 'bigint', 'boolean', 'symbol'],
['undefined', 'null'],
['object', 'array'],
['primitive', 'complex'],
['existent', 'nonexistent'],
['unique']
].forEach(metaPropertyGroup =>
it(`should allow ${new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(metaPropertyGroup.map(metaProperty => `:${metaProperty}`))}`, function() {
metaPropertyGroup.forEach(metaProperty =>
expect(parse.bind(null, `*:${metaProperty}`), `:${metaProperty}`).to.not.throw());
}));
});

describe('Conditions', function() {
it('should allow unary conditions', function() {
expect(parse.bind(null, 'a[b]')).to.not.throw();
Expand Down
Loading

0 comments on commit 3409d3d

Please sign in to comment.