@rimbu/deep
Version:
Tools to use handle plain JS objects as immutable objects
284 lines • 13 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.match = void 0;
var tslib_1 = require("tslib");
var base_1 = require("@rimbu/base");
/**
* Returns true if the given `value` object matches the given `matcher`, false otherwise.
* @typeparam T - the input value type
* @typeparam C - utility type
* @param source - the value to match (should be a plain object)
* @param matcher - a matcher object or a function taking the matcher API and returning a match object
* @param failureLog - (optional) a string array that can be passed to collect reasons why the match failed
* @example
* ```ts
* const input = { a: 1, b: { c: true, d: 'a' } }
* match(input, { a: 1 }) // => true
* match(input, { a: 2 }) // => false
* match(input, { a: (v) => v > 10 }) // => false
* match(input, { b: { c: true }}) // => true
* match(input, (['every', { a: (v) => v > 0 }, { b: { c: true } }]) // => true
* match(input, { b: { c: (v, parent, root) => v && parent.d.length > 0 && root.a > 0 } })
* // => true
* ```
*/
function match(source, matcher, failureLog) {
return matchEntry(source, source, source, matcher, failureLog);
}
exports.match = match;
/**
* Match a generic match entry against the given source.
*/
function matchEntry(source, parent, root, matcher, failureLog) {
if (Object.is(source, matcher)) {
// value and target are exactly the same, always will be true
return true;
}
if (matcher === null || matcher === undefined) {
// these matchers can only be direct matches, and previously it was determined that
// they are not equal
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("value ".concat(JSON.stringify(source), " did not match matcher ").concat(matcher));
return false;
}
if (typeof source === 'function') {
// function source values can only be directly matched
var result = Object.is(source, matcher);
if (!result) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("both value and matcher are functions, but they do not have the same reference");
}
return result;
}
if (typeof matcher === 'function') {
// resolve match function first
var matcherResult = matcher(source, parent, root);
if (typeof matcherResult === 'boolean') {
// function resulted in a direct match result
if (!matcherResult) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("function matcher returned false for value ".concat(JSON.stringify(source)));
}
return matcherResult;
}
// function resulted in a value that needs to be further matched
return matchEntry(source, parent, root, matcherResult, failureLog);
}
if ((0, base_1.isPlainObj)(source)) {
// source ia a plain object, can be partially matched
return matchPlainObj(source, parent, root, matcher, failureLog);
}
if (Array.isArray(source)) {
// source is an array
return matchArr(source, parent, root, matcher, failureLog);
}
// already determined above that the source and matcher are not equal
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("value ".concat(JSON.stringify(source), " does not match given matcher ").concat(JSON.stringify(matcher)));
return false;
}
/**
* Match an array matcher against the given source.
*/
function matchArr(source, parent, root, matcher, failureLog) {
if (Array.isArray(matcher)) {
// directly compare array contents
var length = source.length;
if (length !== matcher.length) {
// if lengths not equal, arrays are not equal
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("array lengths are not equal: value length ".concat(source.length, " !== matcher length ").concat(matcher.length));
return false;
}
// loop over arrays, matching every value
var index = -1;
while (++index < length) {
if (!matchEntry(source[index], source, root, matcher[index], failureLog)) {
// item did not match, return false
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("index ".concat(index, " does not match with value ").concat(JSON.stringify(source[index]), " and matcher ").concat(matcher[index]));
return false;
}
}
// all items are equal
return true;
}
// matcher is plain object
if (typeof matcher === 'object' && null !== matcher) {
if ("every" in matcher) {
return matchCompound(source, parent, root, tslib_1.__spreadArray(['every'], tslib_1.__read(matcher.every), false), failureLog);
}
if ("some" in matcher) {
return matchCompound(source, parent, root, tslib_1.__spreadArray(['some'], tslib_1.__read(matcher.some), false), failureLog);
}
if ("none" in matcher) {
return matchCompound(source, parent, root, tslib_1.__spreadArray(['none'], tslib_1.__read(matcher.none), false), failureLog);
}
if ("single" in matcher) {
return matchCompound(source, parent, root, tslib_1.__spreadArray(['single'], tslib_1.__read(matcher.single), false), failureLog);
}
if ("someItem" in matcher) {
return matchTraversal(source, root, 'someItem', matcher.someItem, failureLog);
}
if ("everyItem" in matcher) {
return matchTraversal(source, root, 'everyItem', matcher.everyItem, failureLog);
}
if ("noneItem" in matcher) {
return matchTraversal(source, root, 'noneItem', matcher.noneItem, failureLog);
}
if ("singleItem" in matcher) {
return matchTraversal(source, root, 'singleItem', matcher.singleItem, failureLog);
}
}
// matcher is plain object with index keys
for (var index in matcher) {
var matcherAtIndex = matcher[index];
if (!(index in source)) {
// source does not have item at given index
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("index ".concat(index, " does not exist in source ").concat(JSON.stringify(source), " but should match matcher ").concat(JSON.stringify(matcherAtIndex)));
return false;
}
// match the source item at the given index
var result = matchEntry(source[index], source, root, matcherAtIndex, failureLog);
if (!result) {
// item did not match
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("index ".concat(index, " does not match with value ").concat(JSON.stringify(source[index]), " and matcher ").concat(JSON.stringify(matcherAtIndex)));
return false;
}
}
// all items match
return true;
}
/**
* Match an object matcher against the given source.
*/
function matchPlainObj(source, parent, root, matcher, failureLog) {
if (Array.isArray(matcher)) {
// the matcher is of compound type
return matchCompound(source, parent, root, matcher, failureLog);
}
// partial object props matcher
for (var key in matcher) {
if (!(key in source)) {
// the source does not have the given key
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("key ".concat(key, " is specified in matcher but not present in value ").concat(JSON.stringify(source)));
return false;
}
// match the source value at the given key with the matcher at given key
var result = matchEntry(source[key], source, root, matcher[key], failureLog);
if (!result) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("key ".concat(key, " does not match in value ").concat(JSON.stringify(source[key]), " with matcher ").concat(JSON.stringify(matcher[key])));
return false;
}
}
// all properties match
return true;
}
/**
* Match a compound matcher against the given source.
*/
function matchCompound(source, parent, root, compound, failureLog) {
// first item indicates compound match type
var matchType = compound[0];
var length = compound.length;
// start at index 1
var index = 0;
switch (matchType) {
case 'every': {
while (++index < length) {
// if any item does not match, return false
var result = matchEntry(source, parent, root, compound[index], failureLog);
if (!result) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in compound \"every\": match at index ".concat(index, " failed"));
return false;
}
}
return true;
}
case 'none': {
// if any item matches, return false
while (++index < length) {
var result = matchEntry(source, parent, root, compound[index], failureLog);
if (result) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in compound \"none\": match at index ".concat(index, " succeeded"));
return false;
}
}
return true;
}
case 'single': {
// if not exactly one item matches, return false
var onePassed = false;
while (++index < length) {
var result = matchEntry(source, parent, root, compound[index], failureLog);
if (result) {
if (onePassed) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in compound \"single\": multiple matches succeeded");
return false;
}
onePassed = true;
}
}
if (!onePassed) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in compound \"single\": no matches succeeded");
}
return onePassed;
}
case 'some': {
// if any item matches, return true
while (++index < length) {
var result = matchEntry(source, parent, root, compound[index], failureLog);
if (result) {
return true;
}
}
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in compound \"some\": no matches succeeded");
return false;
}
}
}
function matchTraversal(source, root, matchType, matcher, failureLog) {
var index = -1;
var length = source.length;
switch (matchType) {
case 'someItem': {
while (++index < length) {
if (matchEntry(source[index], source, root, matcher, failureLog)) {
return true;
}
}
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in array traversal \"someItem\": no items matched given matcher");
return false;
}
case 'everyItem': {
while (++index < length) {
if (!matchEntry(source[index], source, root, matcher, failureLog)) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in array traversal \"everyItem\": at least one item did not match given matcher");
return false;
}
}
return true;
}
case 'noneItem': {
while (++index < length) {
if (matchEntry(source[index], source, root, matcher, failureLog)) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in array traversal \"noneItem\": at least one item matched given matcher");
return false;
}
}
return true;
}
case 'singleItem': {
var singleMatched = false;
while (++index < length) {
if (matchEntry(source[index], source, root, matcher, failureLog)) {
if (singleMatched) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in array traversal \"singleItem\": more than one item matched given matcher");
return false;
}
singleMatched = true;
}
}
if (!singleMatched) {
failureLog === null || failureLog === void 0 ? void 0 : failureLog.push("in array traversal \"singleItem\": no item matched given matcher");
return false;
}
return true;
}
}
}
//# sourceMappingURL=match.cjs.map