UNPKG

@rimbu/deep

Version:

Tools to use handle plain JS objects as immutable objects

279 lines 11.4 kB
import { isPlainObj, } from '@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 * ``` */ export function match(source, matcher, failureLog) { return matchEntry(source, source, source, matcher, failureLog); } /** * 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?.push(`value ${JSON.stringify(source)} did not match matcher ${matcher}`); return false; } if (typeof source === 'function') { // function source values can only be directly matched const result = Object.is(source, matcher); if (!result) { 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 const matcherResult = matcher(source, parent, root); if (typeof matcherResult === 'boolean') { // function resulted in a direct match result if (!matcherResult) { failureLog?.push(`function matcher returned false for value ${JSON.stringify(source)}`); } return matcherResult; } // function resulted in a value that needs to be further matched return matchEntry(source, parent, root, matcherResult, failureLog); } if (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?.push(`value ${JSON.stringify(source)} does not match given matcher ${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 const length = source.length; if (length !== matcher.length) { // if lengths not equal, arrays are not equal failureLog?.push(`array lengths are not equal: value length ${source.length} !== matcher length ${matcher.length}`); return false; } // loop over arrays, matching every value let index = -1; while (++index < length) { if (!matchEntry(source[index], source, root, matcher[index], failureLog)) { // item did not match, return false failureLog?.push(`index ${index} does not match with value ${JSON.stringify(source[index])} and matcher ${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, ['every', ...matcher.every], failureLog); } if (`some` in matcher) { return matchCompound(source, parent, root, ['some', ...matcher.some], failureLog); } if (`none` in matcher) { return matchCompound(source, parent, root, ['none', ...matcher.none], failureLog); } if (`single` in matcher) { return matchCompound(source, parent, root, ['single', ...matcher.single], 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 (const index in matcher) { const matcherAtIndex = matcher[index]; if (!(index in source)) { // source does not have item at given index failureLog?.push(`index ${index} does not exist in source ${JSON.stringify(source)} but should match matcher ${JSON.stringify(matcherAtIndex)}`); return false; } // match the source item at the given index const result = matchEntry(source[index], source, root, matcherAtIndex, failureLog); if (!result) { // item did not match failureLog?.push(`index ${index} does not match with value ${JSON.stringify(source[index])} and matcher ${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 (const key in matcher) { if (!(key in source)) { // the source does not have the given key failureLog?.push(`key ${key} is specified in matcher but not present in value ${JSON.stringify(source)}`); return false; } // match the source value at the given key with the matcher at given key const result = matchEntry(source[key], source, root, matcher[key], failureLog); if (!result) { failureLog?.push(`key ${key} does not match in value ${JSON.stringify(source[key])} with matcher ${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 const matchType = compound[0]; const length = compound.length; // start at index 1 let index = 0; switch (matchType) { case 'every': { while (++index < length) { // if any item does not match, return false const result = matchEntry(source, parent, root, compound[index], failureLog); if (!result) { failureLog?.push(`in compound "every": match at index ${index} failed`); return false; } } return true; } case 'none': { // if any item matches, return false while (++index < length) { const result = matchEntry(source, parent, root, compound[index], failureLog); if (result) { failureLog?.push(`in compound "none": match at index ${index} succeeded`); return false; } } return true; } case 'single': { // if not exactly one item matches, return false let onePassed = false; while (++index < length) { const result = matchEntry(source, parent, root, compound[index], failureLog); if (result) { if (onePassed) { failureLog?.push(`in compound "single": multiple matches succeeded`); return false; } onePassed = true; } } if (!onePassed) { failureLog?.push(`in compound "single": no matches succeeded`); } return onePassed; } case 'some': { // if any item matches, return true while (++index < length) { const result = matchEntry(source, parent, root, compound[index], failureLog); if (result) { return true; } } failureLog?.push(`in compound "some": no matches succeeded`); return false; } } } function matchTraversal(source, root, matchType, matcher, failureLog) { let index = -1; const length = source.length; switch (matchType) { case 'someItem': { while (++index < length) { if (matchEntry(source[index], source, root, matcher, failureLog)) { return true; } } 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?.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?.push(`in array traversal "noneItem": at least one item matched given matcher`); return false; } } return true; } case 'singleItem': { let singleMatched = false; while (++index < length) { if (matchEntry(source[index], source, root, matcher, failureLog)) { if (singleMatched) { failureLog?.push(`in array traversal "singleItem": more than one item matched given matcher`); return false; } singleMatched = true; } } if (!singleMatched) { failureLog?.push(`in array traversal "singleItem": no item matched given matcher`); return false; } return true; } } } //# sourceMappingURL=match.mjs.map