Skip to content

Commit d5bfb80

Browse files
committed
improve support for comparisons
1 parent 65f6c30 commit d5bfb80

File tree

4 files changed

+353
-37
lines changed

4 files changed

+353
-37
lines changed

index.js

+120-35
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,108 @@ const babel = require('@babel/parser');
44
const expression = require('eval-estree-expression');
55
const { evaluate } = expression;
66

7-
const isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v);
7+
const isAST = value => isObject(value) && hasOwnProperty.call(value, 'type');
8+
9+
const isObject = value => {
10+
return value !== null && typeof value === 'object' && !Array.isArray(value);
11+
};
812

913
const isPrimitive = value => {
1014
return value == null || (typeof value !== 'object' && typeof value !== 'function');
1115
};
1216

17+
const LITERALS = {
18+
'undefined': undefined,
19+
'null': null,
20+
'true': true,
21+
'false': false
22+
};
23+
1324
/**
14-
* Returns true if the given value is truthy, or the `left` value is contained within
15-
* the `right` value.
25+
* Returns true if the given value is truthy, or the `value` ("left") is
26+
* equal to or contained within the `context` ("right") value. This method is
27+
* used by the `whence()` function (the main export), but you can use this
28+
* method directly if you don't want the values to be evaluated.
1629
*
1730
* @name equal
18-
* @param {any} `left` The value to test.
19-
* @param {Object} `right` The value to compare against.
31+
* @param {any} `value` The value to test.
32+
* @param {Object} `context` The value to compare against.
2033
* @param {[type]} `parent`
2134
* @return {Boolean} Returns true or false.
2235
* @api public
2336
*/
2437

25-
const equal = (left, right, parent) => {
26-
if (left === right) return true;
38+
const equal = (value, context, options = {}) => {
39+
const eq = (a, b, parent, depth = 0) => {
40+
if (a === b) return true;
2741

28-
if (typeof left === 'boolean' && !parent) {
29-
if (isPrimitive(right)) return left === right;
30-
return left;
31-
}
42+
if (a === 'undefined' || a === 'null') {
43+
return a === b;
44+
}
3245

33-
if (isPrimitive(left) && isObject(right)) {
34-
return Boolean(right[left]);
35-
}
46+
if (typeof a === 'boolean' && (!parent || parent === context)) {
47+
return typeof b === 'boolean' ? a === b : a;
48+
}
3649

37-
if (isPrimitive(left) && Array.isArray(right)) {
38-
return right.includes(left);
39-
}
50+
if ((a === 'true' || a === 'false') && (!parent || parent === context)) {
51+
return a === 'true';
52+
}
4053

41-
if (Array.isArray(left)) {
42-
if (isObject(right)) {
43-
return left.every(ele => equal(ele, right, left));
54+
// only call function values at the root
55+
if (typeof a === 'function' && depth === 0) {
56+
return a.call(b, b, options);
4457
}
4558

46-
if (Array.isArray(right)) {
47-
return left.every((ele, i) => equal(ele, right[i], left));
59+
if (typeof a === 'string' && (isObject(b) && b === context || (depth === 0 && context === undefined))) {
60+
if (options.castBoolean === false || (b && hasOwnProperty.call(b, a))) {
61+
return Boolean(b[a]);
62+
}
63+
64+
return whence.sync(a, b, options);
4865
}
49-
}
5066

51-
if (isObject(left)) {
52-
return isObject(right) && Object.entries(left).every(([k, v]) => equal(v, right[k], left));
53-
}
67+
if (isPrimitive(a) && isObject(b)) {
68+
return Boolean(b[a]);
69+
}
70+
71+
if (isPrimitive(a) && Array.isArray(b)) {
72+
return b.includes(a);
73+
}
74+
75+
if (a instanceof RegExp) {
76+
return !(b instanceof RegExp) ? false : a.toString() === b.toString();
77+
}
5478

55-
return false;
79+
if (a instanceof Date) {
80+
return !(b instanceof Date) ? false : a.toString() === b.toString();
81+
}
82+
83+
if (a instanceof Set) {
84+
return b instanceof Set && eq([...a], [...b], a, depth + 1);
85+
}
86+
87+
if (a instanceof Map) {
88+
return b instanceof Map && [...a].every(([k, v]) => eq(v, b.get(k), a, depth + 1));
89+
}
90+
91+
if (Array.isArray(a)) {
92+
if (isObject(b)) {
93+
return a.every(ele => eq(ele, b, a, depth + 1));
94+
}
95+
96+
if (Array.isArray(b)) {
97+
return a.every((ele, i) => eq(ele, b[i], a, depth + 1));
98+
}
99+
}
100+
101+
if (isObject(a)) {
102+
return isObject(b) && Object.entries(a).every(([k, v]) => eq(v, b[k], a, depth + 1));
103+
}
104+
105+
return false;
106+
};
107+
108+
return eq(value, context);
56109
};
57110

58111
/**
@@ -66,9 +119,9 @@ const equal = (left, right, parent) => {
66119
* // Resuls in something like this:
67120
* // Node {
68121
* // type: 'BinaryExpression',
69-
* // left: Node { type: 'Identifier', name: 'platform' },
122+
* // value: Node { type: 'Identifier', name: 'platform' },
70123
* // operator: '===',
71-
* // right: Node {
124+
* // context: Node {
72125
* // type: 'StringLiteral',
73126
* // extra: { rawValue: 'darwin', raw: '"darwin"' },
74127
* // value: 'darwin'
@@ -115,7 +168,22 @@ const parse = (source, options = {}) => {
115168
* @api public
116169
*/
117170

118-
const whence = (source, context = {}, options = {}) => compile(source, options)(context);
171+
const whence = async (source, context, options = {}) => {
172+
if (isAST(source)) {
173+
return compile(source, options)(context);
174+
}
175+
176+
if (typeof source !== 'string' || (isPrimitive(context) && context !== undefined)) {
177+
return equal(source, context, options);
178+
}
179+
180+
if (hasOwnProperty.call(LITERALS, source)) {
181+
return options.castBoolean !== false ? Boolean(LITERALS[source]) : LITERALS[source];
182+
}
183+
184+
const result = compile(source, options)(context);
185+
return options.castBoolean !== false ? Boolean(await result) : result;
186+
};
119187

120188
/**
121189
* Synchronous version of [whence](#whence). Aliased as `whence.sync()`.
@@ -133,7 +201,22 @@ const whence = (source, context = {}, options = {}) => compile(source, options)(
133201
* @api public
134202
*/
135203

136-
const whenceSync = (source, context = {}, options = {}) => compileSync(source, options)(context);
204+
const whenceSync = (source, context, options = {}) => {
205+
if (isAST(source)) {
206+
return compile.sync(source, options)(context);
207+
}
208+
209+
if (typeof source !== 'string' || (isPrimitive(context) && context !== undefined)) {
210+
return equal(source, context, options);
211+
}
212+
213+
if (hasOwnProperty.call(LITERALS, source)) {
214+
return options.castBoolean !== false ? Boolean(LITERALS[source]) : LITERALS[source];
215+
}
216+
217+
const result = compile.sync(source, options)(context);
218+
return options.castBoolean !== false ? Boolean(result) : result;
219+
};
137220

138221
/**
139222
* Compiles the given expression and returns an async function.
@@ -153,10 +236,11 @@ const whenceSync = (source, context = {}, options = {}) => compileSync(source, o
153236
*/
154237

155238
const compile = (source, options) => {
156-
const ast = parse(source, options);
239+
const opts = { strictVariables: false, booleanLogicalOperators: true, ...options };
240+
const ast = parse(source, opts);
157241

158242
return context => {
159-
return evaluate(ast, context, options);
243+
return evaluate(ast, context, opts);
160244
};
161245
};
162246

@@ -178,10 +262,11 @@ const compile = (source, options) => {
178262
*/
179263

180264
const compileSync = (source, options) => {
181-
const ast = parse(source, options);
265+
const opts = { strictVariables: false, booleanLogicalOperators: true, ...options };
266+
const ast = parse(source, opts);
182267

183268
return context => {
184-
return evaluate.sync(ast, context, options);
269+
return evaluate.sync(ast, context, opts);
185270
};
186271
};
187272

test/equal-whence.js

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
'use strict';
2+
3+
const { strict: assert } = require('assert');
4+
const whence = require('..');
5+
6+
describe('equality with whence()', () => {
7+
describe('primitives', () => {
8+
it('should compare strings', async () => {
9+
assert(await whence('foo', 'foo'));
10+
assert(!await whence('foo', 'bar'));
11+
});
12+
13+
it('should compare booleans', async () => {
14+
assert(await whence(true, {}));
15+
assert(!await whence(false, {}));
16+
assert(await whence(true, true));
17+
assert(!await whence(true, false));
18+
assert(await whence(false, false));
19+
});
20+
21+
it('should compare null and undefined', async () => {
22+
assert(await whence(null, null));
23+
assert(!await whence(null, undefined));
24+
assert(await whence(undefined, undefined));
25+
});
26+
27+
it('should compare numbers', async () => {
28+
assert(await whence(0, 0));
29+
assert(await whence(1, 1));
30+
assert(!await whence(1, 2));
31+
assert(!await whence(0, 1));
32+
});
33+
34+
it('should match object properties with string name', async () => {
35+
assert(await whence('foo', { foo: true }));
36+
assert(await whence('foo', { foo: [] }));
37+
assert(await whence('foo', { foo: [1] }));
38+
assert(!await whence('foo', { foo: false }));
39+
assert(!await whence('foo', { bar: true }));
40+
});
41+
42+
it('should match object properties with symbol name', async () => {
43+
const foo = Symbol('foo');
44+
assert(await whence(foo, { [foo]: true }));
45+
assert(await whence(foo, { [foo]: true }));
46+
assert(await whence(foo, { [foo]: [] }));
47+
assert(await whence(foo, { [foo]: [1] }));
48+
assert(!await whence(foo, { [foo]: false }));
49+
});
50+
});
51+
52+
describe('arrays', () => {
53+
it('should return true when every value in the array is in the context', async () => {
54+
const key = Symbol(':key');
55+
const context = { foo: true, bar: false, baz: 'qux', [key]: true };
56+
assert(!await whence(['foo', 'bar', key], context));
57+
assert(await whence(['foo', 'baz', key], context));
58+
});
59+
});
60+
61+
describe('objects', () => {
62+
it('should return true when every value in the object is in the context', async () => {
63+
const context = { foo: true, bar: false, baz: 'qux' };
64+
assert(await whence({ foo: true }, context));
65+
assert(await whence({ bar: false }, context));
66+
assert(await whence({ bar: false, baz: 'qux' }, context));
67+
assert(!await whence({ bar: false, baz: 'wrong' }, context));
68+
assert(!await whence({ bar: false, other: 'wrong' }, context));
69+
70+
assert(await whence({ baz: 'qux' }, context));
71+
assert(!await whence({ baz: 'fez' }, context));
72+
assert(await whence({ foo: 'bar' }, { foo: 'bar' }));
73+
assert(!await whence({ foo: 'bar' }, { foo: 'baz' }));
74+
assert(await whence({ foo: 'bar', baz: 'qux' }, { foo: 'bar', baz: 'qux' }));
75+
assert(!await whence({ foo: 'bar', baz: 'qux' }, { foo: 'bar' }));
76+
});
77+
78+
it('should work for deeply nested values', async () => {
79+
const context = { a: { b: { c: { d: 'efg' } } } };
80+
assert(await whence({ a: { b: { c: { d: 'efg' } } } }, context));
81+
assert(!await whence({ a: { b: { c: { d: 'efg', extra: true } } } }, context));
82+
assert(!await whence({ a: { b: { c: { d: 'efgh' } } } }, context));
83+
});
84+
});
85+
86+
describe('booleans', () => {
87+
it('should return true equal `true`', async () => {
88+
assert.equal(await whence(true), true);
89+
assert.equal(await whence(true), true);
90+
});
91+
92+
it('should return true equal `"true"`', async () => {
93+
assert.equal(await whence('true'), true);
94+
assert.equal(await whence('true'), true);
95+
});
96+
97+
it('should return true equal `false`', async () => {
98+
assert.equal(await whence(false), false);
99+
assert.equal(await whence(false), false);
100+
});
101+
102+
it('should return false equal `false`', async () => {
103+
assert.equal(await whence('false'), false);
104+
assert.equal(await whence('false'), false);
105+
});
106+
});
107+
108+
describe('conditionals', () => {
109+
it('should support conditionals', async () => {
110+
assert.equal(await whence('9 > 1'), true);
111+
assert.equal(await whence('9 < 1'), false);
112+
});
113+
114+
it('should support conditionals with variables', async () => {
115+
assert.equal(await whence('a > b', { a: 9, b: 1 }), true);
116+
assert.equal(await whence('a < b', { a: 9, b: 1 }), false);
117+
});
118+
});
119+
120+
describe('objects', () => {
121+
it('should support objects', async () => {
122+
assert.equal(await whence({ a: 9 }, { a: 9, b: 1 }), true);
123+
assert.equal(await whence({ a: 8 }, { a: 9, b: 1 }), false);
124+
});
125+
});
126+
127+
describe('dates', () => {
128+
it('should support dates', async () => {
129+
assert.equal(await whence(new Date(), new Date()), true);
130+
assert.equal(await whence(new Date('2020-12-17T03:24:00'), new Date('2020-12-17T03:24:00')), true);
131+
assert.equal(await whence(new Date('2020-12-17T03:24:00'), new Date('2020-11-17T03:24:00')), false);
132+
});
133+
});
134+
135+
describe('undefined', () => {
136+
it('should return true equal undefined', async () => {
137+
assert.equal(await whence(undefined), true);
138+
});
139+
140+
it('should return false equal "undefined"', async () => {
141+
assert.equal(await whence('undefined'), false);
142+
});
143+
});
144+
145+
describe('null', () => {
146+
it('should return false equal null', async () => {
147+
assert.equal(await whence(null), false);
148+
});
149+
150+
it('should return false equal "null"', async () => {
151+
assert.equal(await whence('null'), false);
152+
});
153+
});
154+
});

0 commit comments

Comments
 (0)