@rimbu/deep
Advanced tools
Comparing version 0.11.3 to 0.12.0
@@ -10,13 +10,11 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Tuple = exports.Protected = exports.Path = exports.Match = exports.match = exports.Patch = exports.patchNested = exports.patch = void 0; | ||
exports.Deep = exports.Path = exports.Tuple = void 0; | ||
var tslib_1 = require("tslib"); | ||
var tuple_1 = require("./tuple"); | ||
Object.defineProperty(exports, "Tuple", { enumerable: true, get: function () { return tuple_1.Tuple; } }); | ||
var internal_1 = require("./internal"); | ||
Object.defineProperty(exports, "patch", { enumerable: true, get: function () { return internal_1.patch; } }); | ||
Object.defineProperty(exports, "patchNested", { enumerable: true, get: function () { return internal_1.patchNested; } }); | ||
Object.defineProperty(exports, "Patch", { enumerable: true, get: function () { return internal_1.Patch; } }); | ||
Object.defineProperty(exports, "match", { enumerable: true, get: function () { return internal_1.match; } }); | ||
Object.defineProperty(exports, "Match", { enumerable: true, get: function () { return internal_1.Match; } }); | ||
Object.defineProperty(exports, "Path", { enumerable: true, get: function () { return internal_1.Path; } }); | ||
Object.defineProperty(exports, "Protected", { enumerable: true, get: function () { return internal_1.Protected; } }); | ||
var tuple_1 = require("./tuple"); | ||
Object.defineProperty(exports, "Tuple", { enumerable: true, get: function () { return tuple_1.Tuple; } }); | ||
tslib_1.__exportStar(require("./deep"), exports); | ||
var Deep = tslib_1.__importStar(require("./deep")); | ||
exports.Deep = Deep; | ||
//# sourceMappingURL=index.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Deep = exports.Path = void 0; | ||
var tslib_1 = require("tslib"); | ||
tslib_1.__exportStar(require("./protected"), exports); | ||
tslib_1.__exportStar(require("./match"), exports); | ||
tslib_1.__exportStar(require("./patch"), exports); | ||
tslib_1.__exportStar(require("./path"), exports); | ||
var path_1 = require("./path"); | ||
Object.defineProperty(exports, "Path", { enumerable: true, get: function () { return path_1.Path; } }); | ||
var match_1 = require("./match"); | ||
var Deep = tslib_1.__importStar(require("./deep")); | ||
exports.Deep = Deep; | ||
//# sourceMappingURL=internal.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.match = exports.Match = void 0; | ||
exports.match = void 0; | ||
var base_1 = require("@rimbu/base"); | ||
var Match; | ||
(function (Match) { | ||
/** | ||
* Returns a matcher that returns true if every given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.every({ a: 1, { c: true } } )) // => true | ||
* match(input, Match.every({ a: 1, { c: false } } )) // => false | ||
* ``` | ||
*/ | ||
function every() { | ||
var matchItems = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
matchItems[_i] = arguments[_i]; | ||
} | ||
return new Every(matchItems); | ||
} | ||
Match.every = every; | ||
/** | ||
* Returns a matcher that returns true if at least one of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.some({ a: 5, { c: true } } )) // => true | ||
* match(input, Match.some({ a: 5, { c: false } } )) // => false | ||
* ``` | ||
*/ | ||
function some() { | ||
var matchItems = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
matchItems[_i] = arguments[_i]; | ||
} | ||
return new Some(matchItems); | ||
} | ||
Match.some = some; | ||
/** | ||
* Returns a matcher that returns true if none of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.none({ a: 5, { c: true } } )) // => false | ||
* match(input, Match.none({ a: 5, { c: false } } )) // => true | ||
* ``` | ||
*/ | ||
function none() { | ||
var matchItems = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
matchItems[_i] = arguments[_i]; | ||
} | ||
return new None(matchItems); | ||
} | ||
Match.none = none; | ||
/** | ||
* Returns a matcher that returns true if exactly one of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.single({ a: 1, { c: true } } )) // => false | ||
* match(input, Match.single({ a: 1, { c: false } } )) // => true | ||
* ``` | ||
*/ | ||
function single() { | ||
var matchItems = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
matchItems[_i] = arguments[_i]; | ||
} | ||
return new Single(matchItems); | ||
} | ||
Match.single = single; | ||
})(Match = exports.Match || (exports.Match = {})); | ||
var Every = /** @class */ (function () { | ||
function Every(matchItems) { | ||
this.matchItems = matchItems; | ||
} | ||
return Every; | ||
}()); | ||
var Some = /** @class */ (function () { | ||
function Some(matchItems) { | ||
this.matchItems = matchItems; | ||
} | ||
return Some; | ||
}()); | ||
var None = /** @class */ (function () { | ||
function None(matchItems) { | ||
this.matchItems = matchItems; | ||
} | ||
return None; | ||
}()); | ||
var Single = /** @class */ (function () { | ||
function Single(matchItems) { | ||
this.matchItems = matchItems; | ||
} | ||
return Single; | ||
}()); | ||
/** | ||
* Returns true if the given `value` object matches the given `matcher`, false otherwise. | ||
* @typeparam T - the input value type | ||
* @param value - the value to match (should be a plain object) | ||
* @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 errorCollector - (optional) a string array that can be passed to collect reasons why the match failed | ||
* @example | ||
@@ -126,5 +17,5 @@ * ```ts | ||
* match(input, { a: 2 }) // => false | ||
* match(input, { a: v => v > 10 }) // => false | ||
* match(input, { a: (v) => v > 10 }) // => false | ||
* match(input, { b: { c: true }}) // => true | ||
* match(input, ({ every }) => every({ a: v => v > 0 }, { 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 } }) | ||
@@ -134,115 +25,185 @@ * // => true | ||
*/ | ||
function match(value, matcher) { | ||
if (matcher instanceof Function) { | ||
return matchOptions(value, value, matcher(Match)); | ||
} | ||
return matchOptions(value, value, matcher); | ||
function match(source, matcher, errorCollector) { | ||
if (errorCollector === void 0) { errorCollector = undefined; } | ||
return matchEntry(source, source, source, matcher, errorCollector); | ||
} | ||
exports.match = match; | ||
function matchOptions(value, root, matcher) { | ||
if (matcher instanceof Every) { | ||
var i = -1; | ||
var matchItems = matcher.matchItems; | ||
var len = matchItems.length; | ||
while (++i < len) { | ||
if (!matchOptions(value, root, matchItems[i])) { | ||
return false; | ||
} | ||
} | ||
/** | ||
* Match a generic match entry against the given source. | ||
*/ | ||
function matchEntry(source, parent, root, matcher, errorCollector) { | ||
if (Object.is(source, matcher)) { | ||
// value and target are exactly the same, always will be true | ||
return true; | ||
} | ||
if (matcher instanceof Some) { | ||
var i = -1; | ||
var matchItems = matcher.matchItems; | ||
var len = matchItems.length; | ||
while (++i < len) { | ||
if (matchOptions(value, root, matchItems[i])) { | ||
return true; | ||
if (matcher === null || matcher === undefined) { | ||
// these matchers can only be direct matches, and previously it was determined that | ||
// they are not equal | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push("function matcher returned false for value ".concat(JSON.stringify(source))); | ||
} | ||
return matcherResult; | ||
} | ||
return false; | ||
// function resulted in a value that needs to be further matched | ||
return matchEntry(source, parent, root, matcherResult, errorCollector); | ||
} | ||
if (matcher instanceof None) { | ||
var i = -1; | ||
var matchItems = matcher.matchItems; | ||
var len = matchItems.length; | ||
while (++i < len) { | ||
if (matchOptions(value, root, matchItems[i])) { | ||
if ((0, base_1.isPlainObj)(source)) { | ||
// source ia a plain object, can be partially matched | ||
return matchPlainObj(source, parent, root, matcher, errorCollector); | ||
} | ||
if (Array.isArray(source)) { | ||
// source is an array | ||
return matchArr(source, root, matcher, errorCollector); | ||
} | ||
// already determined above that the source and matcher are not equal | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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, root, matcher, errorCollector) { | ||
if (Array.isArray(matcher)) { | ||
// directly compare array contents | ||
var length_1 = source.length; | ||
if (length_1 !== matcher.length) { | ||
// if lengths not equal, arrays are not equal | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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_1) { | ||
if (!Object.is(source[index], matcher[index])) { | ||
// item did not match, return false | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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; | ||
} | ||
if (matcher instanceof Single) { | ||
var i = -1; | ||
var matchItems = matcher.matchItems; | ||
var len = matchItems.length; | ||
var matched = false; | ||
while (++i < len) { | ||
if (matchOptions(value, root, matchItems[i])) { | ||
if (matched) { | ||
return false; | ||
} | ||
matched = true; | ||
} | ||
// 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 | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push("index ".concat(index, " does not exist in source ").concat(JSON.stringify(source), " but should match matcher ").concat(JSON.stringify(matcherAtIndex))); | ||
return false; | ||
} | ||
return matched; | ||
// match the source item at the given index | ||
var result = matchEntry(source[index], source, root, matcherAtIndex, errorCollector); | ||
if (!result) { | ||
// item did not match | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push("index ".concat(index, " does not match with value ").concat(JSON.stringify(source[index]), " and matcher ").concat(JSON.stringify(matcherAtIndex))); | ||
return false; | ||
} | ||
} | ||
if ((0, base_1.isPlainObj)(matcher)) { | ||
return matchRecord(value, root, matcher); | ||
} | ||
return Object.is(value, matcher); | ||
// all items match | ||
return true; | ||
} | ||
function matchRecord(value, root, matcher) { | ||
if (!(0, base_1.isPlainObj)(matcher)) { | ||
base_1.RimbuError.throwInvalidUsageError('match: to prevent accidental errors, match only supports plain objects as input.'); | ||
/** | ||
* Match an object matcher against the given source. | ||
*/ | ||
function matchPlainObj(source, parent, root, matcher, errorCollector) { | ||
if (Array.isArray(matcher)) { | ||
// the matcher is of compound type | ||
return matchCompound(source, parent, root, matcher, errorCollector); | ||
} | ||
// partial object props matcher | ||
for (var key in matcher) { | ||
if (!(key in value)) | ||
if (!(key in source)) { | ||
// the source does not have the given key | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push("key ".concat(key, " is specified in matcher but not present in value ").concat(JSON.stringify(source))); | ||
return false; | ||
var matchValue = matcher[key]; | ||
var target = value[key]; | ||
if (matchValue instanceof Function) { | ||
if (target instanceof Function && Object.is(target, matchValue)) { | ||
return true; | ||
} | ||
// match the source value at the given key with the matcher at given key | ||
var result = matchEntry(source[key], source, root, matcher[key], errorCollector); | ||
if (!result) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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, errorCollector) { | ||
// 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], errorCollector); | ||
if (!result) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push("in compound \"every\": match at index ".concat(index, " failed")); | ||
return false; | ||
} | ||
} | ||
var result = matchValue(target, value, root); | ||
if (typeof result === 'boolean') { | ||
return true; | ||
} | ||
case 'none': { | ||
// if any item matches, return false | ||
while (++index < length) { | ||
var result = matchEntry(source, parent, root, compound[index], errorCollector); | ||
if (result) { | ||
continue; | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push("in compound \"none\": match at index ".concat(index, " succeeded")); | ||
return false; | ||
} | ||
return false; | ||
} | ||
if (!matchRecordItem(target, root, result)) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
else { | ||
if (!matchRecordItem(target, root, matchValue)) { | ||
return false; | ||
case 'single': { | ||
// if not exactly one item matches, return false | ||
var onePassed = false; | ||
while (++index < length) { | ||
var result = matchEntry(source, parent, root, compound[index], errorCollector); | ||
if (result) { | ||
if (onePassed) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push("in compound \"single\": multiple matches succeeded"); | ||
return false; | ||
} | ||
onePassed = true; | ||
} | ||
} | ||
if (!onePassed) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push("in compound \"single\": no matches succeeded"); | ||
} | ||
return onePassed; | ||
} | ||
} | ||
return true; | ||
} | ||
function matchRecordItem(value, root, matcher) { | ||
if ((0, base_1.isIterable)(matcher) && (0, base_1.isIterable)(value)) { | ||
var it1 = value[Symbol.iterator](); | ||
var it2 = matcher[Symbol.iterator](); | ||
while (true) { | ||
var v1 = it1.next(); | ||
var v2 = it2.next(); | ||
if (v1.done !== v2.done || v1.value !== v2.value) { | ||
return false; | ||
case 'some': { | ||
// if any item matches, return true | ||
while (++index < length) { | ||
var result = matchEntry(source, parent, root, compound[index], errorCollector); | ||
if (result) { | ||
return true; | ||
} | ||
} | ||
if (v1.done) { | ||
return true; | ||
} | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push("in compound \"some\": no matches succeeded"); | ||
return false; | ||
} | ||
} | ||
if ((0, base_1.isPlainObj)(value)) { | ||
return matchOptions(value, root, matcher); | ||
} | ||
return Object.is(value, matcher); | ||
} | ||
//# sourceMappingURL=match.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.patch = exports.patchNested = exports.Patch = void 0; | ||
exports.patch = void 0; | ||
var tslib_1 = require("tslib"); | ||
var base_1 = require("@rimbu/base"); | ||
var Patch; | ||
(function (Patch) { | ||
/** | ||
* Returns a function that patches a given `value` with the given `patchItems`. | ||
* @typeparam T - the patch value type | ||
* @typeparam Q - the input value type | ||
* @param patchItems - a number of `Patch` objects that patch a given value of type T. | ||
* @example | ||
* ```ts | ||
* const items = [{ a: 1, b: 'a' }, { a: 2, b: 'b' }] | ||
* items.map(Patch.create({ a: v => v + 1 })) | ||
* // => [{ a: 2, b: 'a' }, { a: 3, b: 'b' }] | ||
* ``` | ||
*/ | ||
function create() { | ||
var patchItems = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
patchItems[_i] = arguments[_i]; | ||
} | ||
return function (value) { return patch.apply(void 0, tslib_1.__spreadArray([value], tslib_1.__read(patchItems), false)); }; | ||
} | ||
Patch.create = create; | ||
})(Patch = exports.Patch || (exports.Patch = {})); | ||
var NestedObj = /** @class */ (function () { | ||
function NestedObj(patchDataItems) { | ||
this.patchDataItems = patchDataItems; | ||
} | ||
return NestedObj; | ||
}()); | ||
/** | ||
* Returns a nested patch object based on the given `patchDataItems` that work on a subpart | ||
* of a larger object to be patched. | ||
* @typeparam T - the input value type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - the patch type | ||
* @param patchDataItems - a number of `Patch` objects to be applied to the subpart of the object | ||
* @example | ||
* ```ts | ||
* patch({ a: 1, b: { c: true, d: 'a' } }, { b: patchNested({ d: 'b' }) }) | ||
* // => { a: 1, b: { c: true, d: 'b' } } | ||
* ``` | ||
*/ | ||
function patchNested() { | ||
var patchDataItems = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
patchDataItems[_i] = arguments[_i]; | ||
} | ||
return new NestedObj(patchDataItems); | ||
} | ||
exports.patchNested = patchNested; | ||
/** | ||
* Returns an immutably updated version of the given `value` where the given `patchItems` have been | ||
* applied to the result. | ||
* The Rimbu patch notation is as follows: | ||
* - if the target is a simple value or array, the patch can be the same type or a function returning the same type | ||
* - if the target is a tuple (array of fixed length), the patch be the same type or an object containing numeric keys with patches indicating the tuple index to patch | ||
* - if the target is an object, the patch can be the same type, or an array containing partial keys with their patches for the object | ||
* @typeparam T - the type of the value to patch | ||
* @typeparam TE - a utility type | ||
* @typeparam TT - a utility type | ||
* @param value - the input value to patch | ||
* @param patchItems - the `Patch` objects to apply to the input value | ||
* @param patchItem - the `Patch` value to apply to the input value | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* patch(input, { a: 2 }) // => { a: 2, b: { c: true, d: 'a' } } | ||
* patch(input: ($) => ({ b: $({ c: v => !v }) }) ) | ||
* patch(input, [{ a: 2 }]) // => { a: 2, b: { c: true, d: 'a' } } | ||
* patch(input, [{ b: [{ c: (v) => !v }] }] ) | ||
* // => { a: 1, b: { c: false, d: 'a' } } | ||
* patch(input: ($) => ({ a: v => v + 1, b: $({ d: 'q' }) }) ) | ||
* patch(input: [{ a: (v) => v + 1, b: [{ d: 'q' }] }] ) | ||
* // => { a: 2, b: { c: true, d: 'q' } } | ||
* ``` | ||
*/ | ||
function patch(value) { | ||
var patchItems = []; | ||
for (var _i = 1; _i < arguments.length; _i++) { | ||
patchItems[_i - 1] = arguments[_i]; | ||
} | ||
var newValue = (0, base_1.isPlainObj)(value) ? tslib_1.__assign({}, value) : value; | ||
var changedRef = { changed: false }; | ||
var result = processPatch(newValue, newValue, patchItems, changedRef); | ||
if (changedRef.changed) | ||
return result; | ||
return value; | ||
function patch(value, patchItem) { | ||
return patchEntry(value, value, value, patchItem); | ||
} | ||
exports.patch = patch; | ||
function processPatch(value, root, patchDataItems, changedRef) { | ||
var i = -1; | ||
var len = patchDataItems.length; | ||
while (++i < len) { | ||
var patchItem = patchDataItems[i]; | ||
if (patchItem instanceof Function) { | ||
var item = patchItem(patchNested); | ||
if (item instanceof NestedObj) { | ||
processPatch(value, root, item.patchDataItems, changedRef); | ||
} | ||
else { | ||
processPatchObj(value, root, item, changedRef); | ||
} | ||
} | ||
else { | ||
processPatchObj(value, root, patchItem, changedRef); | ||
} | ||
} | ||
return value; | ||
} | ||
function processPatchObj(value, root, patchData, changedRef) { | ||
if (undefined === value || null === value) { | ||
function patchEntry(value, parent, root, patchItem) { | ||
if (Object.is(value, patchItem)) { | ||
// patching a value with itself never changes the value | ||
return value; | ||
} | ||
if (!(0, base_1.isPlainObj)(patchData)) { | ||
base_1.RimbuError.throwInvalidUsageError('patch: received patch object should be a plain object'); | ||
if (typeof value === 'function') { | ||
// function input, directly return patch | ||
return patchItem; | ||
} | ||
if (!(0, base_1.isPlainObj)(value)) { | ||
base_1.RimbuError.throwInvalidUsageError('patch: received source object should be a plain object'); | ||
if (typeof patchItem === 'function') { | ||
// function patch always needs to be resolved first | ||
var item = patchItem(value, parent, root); | ||
return patchEntry(value, parent, root, item); | ||
} | ||
for (var key in patchData) { | ||
var target = value[key]; | ||
// prevent prototype pollution | ||
if (key === '__proto__' || | ||
(key === 'constructor' && target instanceof Function)) { | ||
base_1.RimbuError.throwInvalidUsageError("patch: received patch object key '".concat(key, "' which is not allowed to prevent prototype pollution")); | ||
if ((0, base_1.isPlainObj)(value)) { | ||
// value is plain object | ||
return patchPlainObj(value, root, patchItem); | ||
} | ||
if (Array.isArray(value)) { | ||
// value is tuple or array | ||
return patchArr(value, root, patchItem); | ||
} | ||
// value is primitive type or complex object | ||
return patchItem; | ||
} | ||
function patchPlainObj(value, root, patchItem) { | ||
var e_1, _a; | ||
if (!Array.isArray(patchItem)) { | ||
// the patch is a complete replacement of the current value | ||
return patchItem; | ||
} | ||
// patch is an array of partial updates | ||
// copy the input value | ||
var result = tslib_1.__assign({}, value); | ||
var anyChange = false; | ||
try { | ||
// loop over patches in array | ||
for (var patchItem_1 = tslib_1.__values(patchItem), patchItem_1_1 = patchItem_1.next(); !patchItem_1_1.done; patchItem_1_1 = patchItem_1.next()) { | ||
var entry = patchItem_1_1.value; | ||
// update root if needed | ||
var currentRoot = value === root ? tslib_1.__assign({}, result) : root; | ||
// loop over all the patch keys | ||
for (var key in entry) { | ||
// patch the value at the given key with the patch at that key | ||
var currentValue = result[key]; | ||
var newValue = patchEntry(currentValue, value, currentRoot, entry[key]); | ||
if (!Object.is(currentValue, newValue)) { | ||
// if value changed, set it in result and mark change | ||
anyChange = true; | ||
result[key] = newValue; | ||
} | ||
} | ||
} | ||
var update = patchData[key]; | ||
if (!(key in value) && update instanceof Function) { | ||
base_1.RimbuError.throwInvalidUsageError("patch: received update function object key ".concat(key, " but the key was not present in the source object. Either explicitely set the value in the source to undefined or use a direct value.")); | ||
} | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
if (patchItem_1_1 && !patchItem_1_1.done && (_a = patchItem_1.return)) _a.call(patchItem_1); | ||
} | ||
if (undefined === update) { | ||
base_1.RimbuError.throwInvalidUsageError("patch: received 'undefined' as patch value. Due to type system issues we cannot prevent this through typing, but please use '() => undefined' or '() => yourVar' instead. This value will be ignored for safety."); | ||
} | ||
var newValue = void 0; | ||
if (update instanceof Function) { | ||
newValue = processPatchObjItem(target, root, update(target, value, root), changedRef); | ||
} | ||
else { | ||
newValue = processPatchObjItem(target, root, update, changedRef); | ||
} | ||
if (!Object.is(newValue, target)) { | ||
value[key] = newValue; | ||
changedRef.changed = true; | ||
} | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
if (anyChange) { | ||
// something changed, return new value | ||
return result; | ||
} | ||
// nothing changed, return old value | ||
return value; | ||
} | ||
function processPatchObjItem(value, root, patchResult, superChangedRef) { | ||
if (patchResult instanceof NestedObj) { | ||
var newValue = (0, base_1.isPlainObj)(value) ? tslib_1.__assign({}, value) : value; | ||
var changedRef = { changed: false }; | ||
var result = processPatch(newValue, root, patchResult.patchDataItems, changedRef); | ||
if (changedRef.changed) { | ||
superChangedRef.changed = true; | ||
return result; | ||
function patchArr(value, root, patchItem) { | ||
if (Array.isArray(patchItem)) { | ||
// value is a normal array | ||
// patch is a complete replacement of current array | ||
return patchItem; | ||
} | ||
// value is a tuple | ||
// patch is an object containing numeric keys with function values | ||
// that update the tuple at the given indices | ||
// copy the tuple | ||
var result = tslib_1.__spreadArray([], tslib_1.__read(value), false); | ||
var anyChange = false; | ||
// loop over all index keys in object | ||
for (var index in patchItem) { | ||
var numIndex = index; | ||
// patch the tuple at the given index | ||
var currentValue = result[numIndex]; | ||
var newValue = patchEntry(currentValue, value, root, patchItem[index]); | ||
if (!Object.is(newValue, currentValue)) { | ||
// if value changed, set it in result and mark change | ||
anyChange = true; | ||
result[numIndex] = newValue; | ||
} | ||
return value; | ||
} | ||
return patchResult; | ||
if (anyChange) { | ||
// something changed, return new value | ||
return result; | ||
} | ||
// nothing changed, return old value | ||
return value; | ||
} | ||
//# sourceMappingURL=patch.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Path = void 0; | ||
exports.patchAt = exports.getAt = exports.Path = void 0; | ||
var tslib_1 = require("tslib"); | ||
@@ -9,72 +9,119 @@ var internal_1 = require("./internal"); | ||
/** | ||
* Returns the value resulting from selecting the given `path` in the given `source` object. | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @param source - the object to select in | ||
* @param path - the path into the object | ||
* @example | ||
* ```ts | ||
* console.log(Path.get({ a: { b: { c: 5 } } }), 'a.b') | ||
* // => { c: 5 } | ||
* console.log(Path.get({ a: { b: { c: 5 } } }), 'a.b.c') | ||
* // => 5 | ||
* ``` | ||
* Regular expression used to split a path string into tokens. | ||
*/ | ||
function get(source, path) { | ||
var e_1, _a; | ||
var items = path.split('.'); | ||
var result = source; | ||
try { | ||
for (var items_1 = tslib_1.__values(items), items_1_1 = items_1.next(); !items_1_1.done; items_1_1 = items_1.next()) { | ||
var item = items_1_1.value; | ||
result = result[item]; | ||
Path.stringSplitRegex = /\?\.|\.|\[|\]/g; | ||
/** | ||
* Return the given `path` string split into an array of subpaths. | ||
* @param path - the input string path | ||
*/ | ||
function stringSplit(path) { | ||
return path.split(Path.stringSplitRegex); | ||
} | ||
Path.stringSplit = stringSplit; | ||
})(Path = exports.Path || (exports.Path = {})); | ||
/** | ||
* Returns the value resulting from selecting the given `path` in the given `source` object. | ||
* It supports optional chaining for nullable values or values that may be undefined, and also | ||
* for accessing objects inside an array. | ||
* There is currently no support for forcing non-null (the `!` operator). | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @param source - the object to select in | ||
* @param path - the path into the object | ||
* @example | ||
* ```ts | ||
* const value = { a: { b: { c: [{ d: 5 }, { d: 6 }] } } } | ||
* Deep.getAt(value, 'a.b'); | ||
* // => { c: 5 } | ||
* Deep.getAt(value, 'a.b.c'); | ||
* // => [{ d: 5 }, { d: 5 }] | ||
* Deep.getAt(value, 'a.b.c[1]'); | ||
* // => { d: 6 } | ||
* Deep.getAt(value, 'a.b.c[1]?.d'); | ||
* // => 6 | ||
* ``` | ||
*/ | ||
function getAt(source, path) { | ||
var e_1, _a; | ||
if (path === '') { | ||
// empty path always directly returns source value | ||
return source; | ||
} | ||
var items = Path.stringSplit(path); | ||
// start with `source` as result value | ||
var result = source; | ||
try { | ||
for (var items_1 = tslib_1.__values(items), items_1_1 = items_1.next(); !items_1_1.done; items_1_1 = items_1.next()) { | ||
var item = items_1_1.value; | ||
if (undefined === item || item === '' || item === '[') { | ||
// ignore irrelevant items | ||
continue; | ||
} | ||
} | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
if (items_1_1 && !items_1_1.done && (_a = items_1.return)) _a.call(items_1); | ||
if (undefined === result || null === result) { | ||
// optional chaining assumed and no value available, skip rest of path and return undefined | ||
return undefined; | ||
} | ||
finally { if (e_1) throw e_1.error; } | ||
// set current result to subpath value | ||
result = result[item]; | ||
} | ||
return result; | ||
} | ||
Path.get = get; | ||
/** | ||
* Sets the value at the given path in the source to the given value. | ||
* @param source - the object to update | ||
* @param path - the path in the object to update | ||
* @param value - the new value to set at the given position | ||
* @example | ||
* ```ts | ||
* console.log(Path.update({ a: { b: { c: 5 } } }, 'a.b.c', v => v + 5) | ||
* // => { a: { b: { c: 6 } } } | ||
* ``` | ||
*/ | ||
function update(source, path, value) { | ||
var e_2, _a; | ||
var items = path.split('.'); | ||
var last = items.pop(); | ||
var root = {}; | ||
var current = root; | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
for (var items_2 = tslib_1.__values(items), items_2_1 = items_2.next(); !items_2_1.done; items_2_1 = items_2.next()) { | ||
var item = items_2_1.value; | ||
var next = {}; | ||
current[item] = (0, internal_1.patchNested)(next); | ||
current = next; | ||
} | ||
if (items_1_1 && !items_1_1.done && (_a = items_1.return)) _a.call(items_1); | ||
} | ||
catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
finally { | ||
try { | ||
if (items_2_1 && !items_2_1.done && (_a = items_2.return)) _a.call(items_2); | ||
} | ||
finally { if (e_2) throw e_2.error; } | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
return result; | ||
} | ||
exports.getAt = getAt; | ||
/** | ||
* Patches the value at the given path in the source to the given value. | ||
* Because the path to update must exist in the `source` object, optional | ||
* chaining and array indexing is not allowed. | ||
* @param source - the object to update | ||
* @param path - the path in the object to update | ||
* @param patchItem - the patch for the value at the given path | ||
* @example | ||
* ```ts | ||
* const value = { a: { b: { c: 5 } } }; | ||
* Deep.patchAt(value, 'a.b.c', v => v + 5); | ||
* // => { a: { b: { c: 6 } } } | ||
* ``` | ||
*/ | ||
function patchAt(source, path, patchItem) { | ||
if (path === '') { | ||
return internal_1.Deep.patch(source, patchItem); | ||
} | ||
var items = Path.stringSplit(path); | ||
// creates a patch object based on the current path | ||
function createPatchPart(index, target) { | ||
var _a; | ||
if (index === items.length) { | ||
// processed all items, return the input `patchItem` | ||
return patchItem; | ||
} | ||
current[last] = value; | ||
return (0, internal_1.patch)(source, root); | ||
var item = items[index]; | ||
if (undefined === item || item === '') { | ||
// empty items can be ignored | ||
return createPatchPart(index + 1, target); | ||
} | ||
if (item === '[') { | ||
// next item is array index, set arrayMode to true | ||
return createPatchPart(index + 1, target); | ||
} | ||
// create object with subPart as property key, and the restuls of processing next parts as value | ||
var result = (_a = {}, | ||
_a[item] = createPatchPart(index + 1, target[item]), | ||
_a); | ||
if (Array.isArray(target)) { | ||
// target in source object is array/tuple, so the patch should be object | ||
return result; | ||
} | ||
// target in source is not an array, so it patch should be an array | ||
return [result]; | ||
} | ||
Path.update = update; | ||
})(Path = exports.Path || (exports.Path = {})); | ||
return internal_1.Deep.patch(source, createPatchPart(0, source)); | ||
} | ||
exports.patchAt = patchAt; | ||
//# sourceMappingURL=path.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Protected = void 0; | ||
/** | ||
* Returns the same value wrapped in the `Protected` type. | ||
* @param value - the value to wrap | ||
* @note does not perform any runtime protection, it is only a utility to easily add the `Protected` | ||
* type to a value | ||
* @example | ||
* ```ts | ||
* const obj = Protected({ a: 1, b: { c: true, d: [1] } }) | ||
* obj.a = 2 // compiler error: a is readonly | ||
* obj.b.c = false // compiler error: c is readonly | ||
* obj.b.d.push(2) // compiler error: d is a readonly array | ||
* (obj as any).b.d.push(2) // will actually mutate the object | ||
* ``` | ||
*/ | ||
function Protected(value) { | ||
return value; | ||
} | ||
exports.Protected = Protected; | ||
//# sourceMappingURL=protected.js.map |
@@ -8,4 +8,12 @@ /** | ||
*/ | ||
export { patch, patchNested, Patch, match, Match, Path, Protected, } from './internal'; | ||
export { Tuple } from './tuple'; | ||
export { Path } from './internal'; | ||
export * from './deep'; | ||
import * as Deep from './deep'; | ||
export { | ||
/** | ||
* Convenience namespace offering access to most common functions used in the `@rimbu/deep` package. | ||
* These are mainly utilities to patch and match plain JavaScript objects. | ||
*/ | ||
Deep, }; | ||
//# sourceMappingURL=index.js.map |
@@ -1,5 +0,5 @@ | ||
export * from './protected'; | ||
export * from './match'; | ||
export * from './patch'; | ||
export * from './path'; | ||
export { Path } from './path'; | ||
export {} from './match'; | ||
import * as Deep from './deep'; | ||
export { Deep }; | ||
//# sourceMappingURL=internal.js.map |
@@ -1,98 +0,9 @@ | ||
import { RimbuError, isPlainObj, isIterable, } from '@rimbu/base'; | ||
export var Match; | ||
(function (Match) { | ||
/** | ||
* Returns a matcher that returns true if every given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.every({ a: 1, { c: true } } )) // => true | ||
* match(input, Match.every({ a: 1, { c: false } } )) // => false | ||
* ``` | ||
*/ | ||
function every(...matchItems) { | ||
return new Every(matchItems); | ||
} | ||
Match.every = every; | ||
/** | ||
* Returns a matcher that returns true if at least one of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.some({ a: 5, { c: true } } )) // => true | ||
* match(input, Match.some({ a: 5, { c: false } } )) // => false | ||
* ``` | ||
*/ | ||
function some(...matchItems) { | ||
return new Some(matchItems); | ||
} | ||
Match.some = some; | ||
/** | ||
* Returns a matcher that returns true if none of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.none({ a: 5, { c: true } } )) // => false | ||
* match(input, Match.none({ a: 5, { c: false } } )) // => true | ||
* ``` | ||
*/ | ||
function none(...matchItems) { | ||
return new None(matchItems); | ||
} | ||
Match.none = none; | ||
/** | ||
* Returns a matcher that returns true if exactly one of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.single({ a: 1, { c: true } } )) // => false | ||
* match(input, Match.single({ a: 1, { c: false } } )) // => true | ||
* ``` | ||
*/ | ||
function single(...matchItems) { | ||
return new Single(matchItems); | ||
} | ||
Match.single = single; | ||
})(Match || (Match = {})); | ||
class Every { | ||
constructor(matchItems) { | ||
this.matchItems = matchItems; | ||
} | ||
} | ||
class Some { | ||
constructor(matchItems) { | ||
this.matchItems = matchItems; | ||
} | ||
} | ||
class None { | ||
constructor(matchItems) { | ||
this.matchItems = matchItems; | ||
} | ||
} | ||
class Single { | ||
constructor(matchItems) { | ||
this.matchItems = matchItems; | ||
} | ||
} | ||
import { isPlainObj, } from '@rimbu/base'; | ||
/** | ||
* Returns true if the given `value` object matches the given `matcher`, false otherwise. | ||
* @typeparam T - the input value type | ||
* @param value - the value to match (should be a plain object) | ||
* @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 errorCollector - (optional) a string array that can be passed to collect reasons why the match failed | ||
* @example | ||
@@ -103,5 +14,5 @@ * ```ts | ||
* match(input, { a: 2 }) // => false | ||
* match(input, { a: v => v > 10 }) // => false | ||
* match(input, { a: (v) => v > 10 }) // => false | ||
* match(input, { b: { c: true }}) // => true | ||
* match(input, ({ every }) => every({ a: v => v > 0 }, { 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 } }) | ||
@@ -111,114 +22,183 @@ * // => true | ||
*/ | ||
export function match(value, matcher) { | ||
if (matcher instanceof Function) { | ||
return matchOptions(value, value, matcher(Match)); | ||
} | ||
return matchOptions(value, value, matcher); | ||
export function match(source, matcher, errorCollector = undefined) { | ||
return matchEntry(source, source, source, matcher, errorCollector); | ||
} | ||
function matchOptions(value, root, matcher) { | ||
if (matcher instanceof Every) { | ||
let i = -1; | ||
const { matchItems } = matcher; | ||
const len = matchItems.length; | ||
while (++i < len) { | ||
if (!matchOptions(value, root, matchItems[i])) { | ||
return false; | ||
} | ||
} | ||
/** | ||
* Match a generic match entry against the given source. | ||
*/ | ||
function matchEntry(source, parent, root, matcher, errorCollector) { | ||
if (Object.is(source, matcher)) { | ||
// value and target are exactly the same, always will be true | ||
return true; | ||
} | ||
if (matcher instanceof Some) { | ||
let i = -1; | ||
const { matchItems } = matcher; | ||
const len = matchItems.length; | ||
while (++i < len) { | ||
if (matchOptions(value, root, matchItems[i])) { | ||
return true; | ||
if (matcher === null || matcher === undefined) { | ||
// these matchers can only be direct matches, and previously it was determined that | ||
// they are not equal | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`function matcher returned false for value ${JSON.stringify(source)}`); | ||
} | ||
return matcherResult; | ||
} | ||
return false; | ||
// function resulted in a value that needs to be further matched | ||
return matchEntry(source, parent, root, matcherResult, errorCollector); | ||
} | ||
if (matcher instanceof None) { | ||
let i = -1; | ||
const { matchItems } = matcher; | ||
const len = matchItems.length; | ||
while (++i < len) { | ||
if (matchOptions(value, root, matchItems[i])) { | ||
if (isPlainObj(source)) { | ||
// source ia a plain object, can be partially matched | ||
return matchPlainObj(source, parent, root, matcher, errorCollector); | ||
} | ||
if (Array.isArray(source)) { | ||
// source is an array | ||
return matchArr(source, root, matcher, errorCollector); | ||
} | ||
// already determined above that the source and matcher are not equal | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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, root, matcher, errorCollector) { | ||
if (Array.isArray(matcher)) { | ||
// directly compare array contents | ||
const length = source.length; | ||
if (length !== matcher.length) { | ||
// if lengths not equal, arrays are not equal | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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 (!Object.is(source[index], matcher[index])) { | ||
// item did not match, return false | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`index ${index} does not match with value ${JSON.stringify(source[index])} and matcher ${matcher[index]}`); | ||
return false; | ||
} | ||
} | ||
// all items are equal | ||
return true; | ||
} | ||
if (matcher instanceof Single) { | ||
let i = -1; | ||
const { matchItems } = matcher; | ||
const len = matchItems.length; | ||
let matched = false; | ||
while (++i < len) { | ||
if (matchOptions(value, root, matchItems[i])) { | ||
if (matched) { | ||
return false; | ||
} | ||
matched = true; | ||
} | ||
// 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 | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`index ${index} does not exist in source ${JSON.stringify(source)} but should match matcher ${JSON.stringify(matcherAtIndex)}`); | ||
return false; | ||
} | ||
return matched; | ||
// match the source item at the given index | ||
const result = matchEntry(source[index], source, root, matcherAtIndex, errorCollector); | ||
if (!result) { | ||
// item did not match | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`index ${index} does not match with value ${JSON.stringify(source[index])} and matcher ${JSON.stringify(matcherAtIndex)}`); | ||
return false; | ||
} | ||
} | ||
if (isPlainObj(matcher)) { | ||
return matchRecord(value, root, matcher); | ||
} | ||
return Object.is(value, matcher); | ||
// all items match | ||
return true; | ||
} | ||
function matchRecord(value, root, matcher) { | ||
if (!isPlainObj(matcher)) { | ||
RimbuError.throwInvalidUsageError('match: to prevent accidental errors, match only supports plain objects as input.'); | ||
/** | ||
* Match an object matcher against the given source. | ||
*/ | ||
function matchPlainObj(source, parent, root, matcher, errorCollector) { | ||
if (Array.isArray(matcher)) { | ||
// the matcher is of compound type | ||
return matchCompound(source, parent, root, matcher, errorCollector); | ||
} | ||
// partial object props matcher | ||
for (const key in matcher) { | ||
if (!(key in value)) | ||
if (!(key in source)) { | ||
// the source does not have the given key | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`key ${key} is specified in matcher but not present in value ${JSON.stringify(source)}`); | ||
return false; | ||
const matchValue = matcher[key]; | ||
const target = value[key]; | ||
if (matchValue instanceof Function) { | ||
if (target instanceof Function && Object.is(target, matchValue)) { | ||
return true; | ||
} | ||
// match the source value at the given key with the matcher at given key | ||
const result = matchEntry(source[key], source, root, matcher[key], errorCollector); | ||
if (!result) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.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, errorCollector) { | ||
// 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], errorCollector); | ||
if (!result) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`in compound "every": match at index ${index} failed`); | ||
return false; | ||
} | ||
} | ||
const result = matchValue(target, value, root); | ||
if (typeof result === 'boolean') { | ||
return true; | ||
} | ||
case 'none': { | ||
// if any item matches, return false | ||
while (++index < length) { | ||
const result = matchEntry(source, parent, root, compound[index], errorCollector); | ||
if (result) { | ||
continue; | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`in compound "none": match at index ${index} succeeded`); | ||
return false; | ||
} | ||
return false; | ||
} | ||
if (!matchRecordItem(target, root, result)) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
else { | ||
if (!matchRecordItem(target, root, matchValue)) { | ||
return false; | ||
case 'single': { | ||
// if not exactly one item matches, return false | ||
let onePassed = false; | ||
while (++index < length) { | ||
const result = matchEntry(source, parent, root, compound[index], errorCollector); | ||
if (result) { | ||
if (onePassed) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`in compound "single": multiple matches succeeded`); | ||
return false; | ||
} | ||
onePassed = true; | ||
} | ||
} | ||
if (!onePassed) { | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`in compound "single": no matches succeeded`); | ||
} | ||
return onePassed; | ||
} | ||
} | ||
return true; | ||
} | ||
function matchRecordItem(value, root, matcher) { | ||
if (isIterable(matcher) && isIterable(value)) { | ||
const it1 = value[Symbol.iterator](); | ||
const it2 = matcher[Symbol.iterator](); | ||
while (true) { | ||
const v1 = it1.next(); | ||
const v2 = it2.next(); | ||
if (v1.done !== v2.done || v1.value !== v2.value) { | ||
return false; | ||
case 'some': { | ||
// if any item matches, return true | ||
while (++index < length) { | ||
const result = matchEntry(source, parent, root, compound[index], errorCollector); | ||
if (result) { | ||
return true; | ||
} | ||
} | ||
if (v1.done) { | ||
return true; | ||
} | ||
errorCollector === null || errorCollector === void 0 ? void 0 : errorCollector.push(`in compound "some": no matches succeeded`); | ||
return false; | ||
} | ||
} | ||
if (isPlainObj(value)) { | ||
return matchOptions(value, root, matcher); | ||
} | ||
return Object.is(value, matcher); | ||
} | ||
//# sourceMappingURL=match.js.map |
@@ -1,136 +0,115 @@ | ||
import { RimbuError, isPlainObj, } from '@rimbu/base'; | ||
export var Patch; | ||
(function (Patch) { | ||
/** | ||
* Returns a function that patches a given `value` with the given `patchItems`. | ||
* @typeparam T - the patch value type | ||
* @typeparam Q - the input value type | ||
* @param patchItems - a number of `Patch` objects that patch a given value of type T. | ||
* @example | ||
* ```ts | ||
* const items = [{ a: 1, b: 'a' }, { a: 2, b: 'b' }] | ||
* items.map(Patch.create({ a: v => v + 1 })) | ||
* // => [{ a: 2, b: 'a' }, { a: 3, b: 'b' }] | ||
* ``` | ||
*/ | ||
function create(...patchItems) { | ||
return (value) => patch(value, ...patchItems); | ||
} | ||
Patch.create = create; | ||
})(Patch || (Patch = {})); | ||
class NestedObj { | ||
constructor(patchDataItems) { | ||
this.patchDataItems = patchDataItems; | ||
} | ||
} | ||
import { isPlainObj } from '@rimbu/base'; | ||
/** | ||
* Returns a nested patch object based on the given `patchDataItems` that work on a subpart | ||
* of a larger object to be patched. | ||
* @typeparam T - the input value type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - the patch type | ||
* @param patchDataItems - a number of `Patch` objects to be applied to the subpart of the object | ||
* @example | ||
* ```ts | ||
* patch({ a: 1, b: { c: true, d: 'a' } }, { b: patchNested({ d: 'b' }) }) | ||
* // => { a: 1, b: { c: true, d: 'b' } } | ||
* ``` | ||
*/ | ||
export function patchNested(...patchDataItems) { | ||
return new NestedObj(patchDataItems); | ||
} | ||
/** | ||
* Returns an immutably updated version of the given `value` where the given `patchItems` have been | ||
* applied to the result. | ||
* The Rimbu patch notation is as follows: | ||
* - if the target is a simple value or array, the patch can be the same type or a function returning the same type | ||
* - if the target is a tuple (array of fixed length), the patch be the same type or an object containing numeric keys with patches indicating the tuple index to patch | ||
* - if the target is an object, the patch can be the same type, or an array containing partial keys with their patches for the object | ||
* @typeparam T - the type of the value to patch | ||
* @typeparam TE - a utility type | ||
* @typeparam TT - a utility type | ||
* @param value - the input value to patch | ||
* @param patchItems - the `Patch` objects to apply to the input value | ||
* @param patchItem - the `Patch` value to apply to the input value | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* patch(input, { a: 2 }) // => { a: 2, b: { c: true, d: 'a' } } | ||
* patch(input: ($) => ({ b: $({ c: v => !v }) }) ) | ||
* patch(input, [{ a: 2 }]) // => { a: 2, b: { c: true, d: 'a' } } | ||
* patch(input, [{ b: [{ c: (v) => !v }] }] ) | ||
* // => { a: 1, b: { c: false, d: 'a' } } | ||
* patch(input: ($) => ({ a: v => v + 1, b: $({ d: 'q' }) }) ) | ||
* patch(input: [{ a: (v) => v + 1, b: [{ d: 'q' }] }] ) | ||
* // => { a: 2, b: { c: true, d: 'q' } } | ||
* ``` | ||
*/ | ||
export function patch(value, ...patchItems) { | ||
const newValue = isPlainObj(value) ? Object.assign({}, value) : value; | ||
const changedRef = { changed: false }; | ||
const result = processPatch(newValue, newValue, patchItems, changedRef); | ||
if (changedRef.changed) | ||
return result; | ||
return value; | ||
export function patch(value, patchItem) { | ||
return patchEntry(value, value, value, patchItem); | ||
} | ||
function processPatch(value, root, patchDataItems, changedRef) { | ||
let i = -1; | ||
const len = patchDataItems.length; | ||
while (++i < len) { | ||
const patchItem = patchDataItems[i]; | ||
if (patchItem instanceof Function) { | ||
const item = patchItem(patchNested); | ||
if (item instanceof NestedObj) { | ||
processPatch(value, root, item.patchDataItems, changedRef); | ||
} | ||
else { | ||
processPatchObj(value, root, item, changedRef); | ||
} | ||
} | ||
else { | ||
processPatchObj(value, root, patchItem, changedRef); | ||
} | ||
} | ||
return value; | ||
} | ||
function processPatchObj(value, root, patchData, changedRef) { | ||
if (undefined === value || null === value) { | ||
function patchEntry(value, parent, root, patchItem) { | ||
if (Object.is(value, patchItem)) { | ||
// patching a value with itself never changes the value | ||
return value; | ||
} | ||
if (!isPlainObj(patchData)) { | ||
RimbuError.throwInvalidUsageError('patch: received patch object should be a plain object'); | ||
if (typeof value === 'function') { | ||
// function input, directly return patch | ||
return patchItem; | ||
} | ||
if (!isPlainObj(value)) { | ||
RimbuError.throwInvalidUsageError('patch: received source object should be a plain object'); | ||
if (typeof patchItem === 'function') { | ||
// function patch always needs to be resolved first | ||
const item = patchItem(value, parent, root); | ||
return patchEntry(value, parent, root, item); | ||
} | ||
for (const key in patchData) { | ||
const target = value[key]; | ||
// prevent prototype pollution | ||
if (key === '__proto__' || | ||
(key === 'constructor' && target instanceof Function)) { | ||
RimbuError.throwInvalidUsageError(`patch: received patch object key '${key}' which is not allowed to prevent prototype pollution`); | ||
if (isPlainObj(value)) { | ||
// value is plain object | ||
return patchPlainObj(value, root, patchItem); | ||
} | ||
if (Array.isArray(value)) { | ||
// value is tuple or array | ||
return patchArr(value, root, patchItem); | ||
} | ||
// value is primitive type or complex object | ||
return patchItem; | ||
} | ||
function patchPlainObj(value, root, patchItem) { | ||
if (!Array.isArray(patchItem)) { | ||
// the patch is a complete replacement of the current value | ||
return patchItem; | ||
} | ||
// patch is an array of partial updates | ||
// copy the input value | ||
const result = Object.assign({}, value); | ||
let anyChange = false; | ||
// loop over patches in array | ||
for (const entry of patchItem) { | ||
// update root if needed | ||
const currentRoot = value === root ? Object.assign({}, result) : root; | ||
// loop over all the patch keys | ||
for (const key in entry) { | ||
// patch the value at the given key with the patch at that key | ||
const currentValue = result[key]; | ||
const newValue = patchEntry(currentValue, value, currentRoot, entry[key]); | ||
if (!Object.is(currentValue, newValue)) { | ||
// if value changed, set it in result and mark change | ||
anyChange = true; | ||
result[key] = newValue; | ||
} | ||
} | ||
const update = patchData[key]; | ||
if (!(key in value) && update instanceof Function) { | ||
RimbuError.throwInvalidUsageError(`patch: received update function object key ${key} but the key was not present in the source object. Either explicitely set the value in the source to undefined or use a direct value.`); | ||
} | ||
if (undefined === update) { | ||
RimbuError.throwInvalidUsageError("patch: received 'undefined' as patch value. Due to type system issues we cannot prevent this through typing, but please use '() => undefined' or '() => yourVar' instead. This value will be ignored for safety."); | ||
} | ||
let newValue; | ||
if (update instanceof Function) { | ||
newValue = processPatchObjItem(target, root, update(target, value, root), changedRef); | ||
} | ||
else { | ||
newValue = processPatchObjItem(target, root, update, changedRef); | ||
} | ||
if (!Object.is(newValue, target)) { | ||
value[key] = newValue; | ||
changedRef.changed = true; | ||
} | ||
} | ||
if (anyChange) { | ||
// something changed, return new value | ||
return result; | ||
} | ||
// nothing changed, return old value | ||
return value; | ||
} | ||
function processPatchObjItem(value, root, patchResult, superChangedRef) { | ||
if (patchResult instanceof NestedObj) { | ||
const newValue = isPlainObj(value) ? Object.assign({}, value) : value; | ||
const changedRef = { changed: false }; | ||
const result = processPatch(newValue, root, patchResult.patchDataItems, changedRef); | ||
if (changedRef.changed) { | ||
superChangedRef.changed = true; | ||
return result; | ||
function patchArr(value, root, patchItem) { | ||
if (Array.isArray(patchItem)) { | ||
// value is a normal array | ||
// patch is a complete replacement of current array | ||
return patchItem; | ||
} | ||
// value is a tuple | ||
// patch is an object containing numeric keys with function values | ||
// that update the tuple at the given indices | ||
// copy the tuple | ||
const result = [...value]; | ||
let anyChange = false; | ||
// loop over all index keys in object | ||
for (const index in patchItem) { | ||
const numIndex = index; | ||
// patch the tuple at the given index | ||
const currentValue = result[numIndex]; | ||
const newValue = patchEntry(currentValue, value, root, patchItem[index]); | ||
if (!Object.is(newValue, currentValue)) { | ||
// if value changed, set it in result and mark change | ||
anyChange = true; | ||
result[numIndex] = newValue; | ||
} | ||
return value; | ||
} | ||
return patchResult; | ||
if (anyChange) { | ||
// something changed, return new value | ||
return result; | ||
} | ||
// nothing changed, return old value | ||
return value; | ||
} | ||
//# sourceMappingURL=patch.js.map |
@@ -1,53 +0,108 @@ | ||
import { patch, patchNested } from './internal'; | ||
import { Deep } from './internal'; | ||
export var Path; | ||
(function (Path) { | ||
/** | ||
* Returns the value resulting from selecting the given `path` in the given `source` object. | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @param source - the object to select in | ||
* @param path - the path into the object | ||
* @example | ||
* ```ts | ||
* console.log(Path.get({ a: { b: { c: 5 } } }), 'a.b') | ||
* // => { c: 5 } | ||
* console.log(Path.get({ a: { b: { c: 5 } } }), 'a.b.c') | ||
* // => 5 | ||
* ``` | ||
* Regular expression used to split a path string into tokens. | ||
*/ | ||
function get(source, path) { | ||
const items = path.split('.'); | ||
let result = source; | ||
for (const item of items) { | ||
result = result[item]; | ||
} | ||
return result; | ||
} | ||
Path.get = get; | ||
Path.stringSplitRegex = /\?\.|\.|\[|\]/g; | ||
/** | ||
* Sets the value at the given path in the source to the given value. | ||
* @param source - the object to update | ||
* @param path - the path in the object to update | ||
* @param value - the new value to set at the given position | ||
* @example | ||
* ```ts | ||
* console.log(Path.update({ a: { b: { c: 5 } } }, 'a.b.c', v => v + 5) | ||
* // => { a: { b: { c: 6 } } } | ||
* ``` | ||
* Return the given `path` string split into an array of subpaths. | ||
* @param path - the input string path | ||
*/ | ||
function update(source, path, value) { | ||
const items = path.split('.'); | ||
const last = items.pop(); | ||
const root = {}; | ||
let current = root; | ||
for (const item of items) { | ||
const next = {}; | ||
current[item] = patchNested(next); | ||
current = next; | ||
function stringSplit(path) { | ||
return path.split(Path.stringSplitRegex); | ||
} | ||
Path.stringSplit = stringSplit; | ||
})(Path || (Path = {})); | ||
/** | ||
* Returns the value resulting from selecting the given `path` in the given `source` object. | ||
* It supports optional chaining for nullable values or values that may be undefined, and also | ||
* for accessing objects inside an array. | ||
* There is currently no support for forcing non-null (the `!` operator). | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @param source - the object to select in | ||
* @param path - the path into the object | ||
* @example | ||
* ```ts | ||
* const value = { a: { b: { c: [{ d: 5 }, { d: 6 }] } } } | ||
* Deep.getAt(value, 'a.b'); | ||
* // => { c: 5 } | ||
* Deep.getAt(value, 'a.b.c'); | ||
* // => [{ d: 5 }, { d: 5 }] | ||
* Deep.getAt(value, 'a.b.c[1]'); | ||
* // => { d: 6 } | ||
* Deep.getAt(value, 'a.b.c[1]?.d'); | ||
* // => 6 | ||
* ``` | ||
*/ | ||
export function getAt(source, path) { | ||
if (path === '') { | ||
// empty path always directly returns source value | ||
return source; | ||
} | ||
const items = Path.stringSplit(path); | ||
// start with `source` as result value | ||
let result = source; | ||
for (const item of items) { | ||
if (undefined === item || item === '' || item === '[') { | ||
// ignore irrelevant items | ||
continue; | ||
} | ||
current[last] = value; | ||
return patch(source, root); | ||
if (undefined === result || null === result) { | ||
// optional chaining assumed and no value available, skip rest of path and return undefined | ||
return undefined; | ||
} | ||
// set current result to subpath value | ||
result = result[item]; | ||
} | ||
Path.update = update; | ||
})(Path || (Path = {})); | ||
return result; | ||
} | ||
/** | ||
* Patches the value at the given path in the source to the given value. | ||
* Because the path to update must exist in the `source` object, optional | ||
* chaining and array indexing is not allowed. | ||
* @param source - the object to update | ||
* @param path - the path in the object to update | ||
* @param patchItem - the patch for the value at the given path | ||
* @example | ||
* ```ts | ||
* const value = { a: { b: { c: 5 } } }; | ||
* Deep.patchAt(value, 'a.b.c', v => v + 5); | ||
* // => { a: { b: { c: 6 } } } | ||
* ``` | ||
*/ | ||
export function patchAt(source, path, patchItem) { | ||
if (path === '') { | ||
return Deep.patch(source, patchItem); | ||
} | ||
const items = Path.stringSplit(path); | ||
// creates a patch object based on the current path | ||
function createPatchPart(index, target) { | ||
if (index === items.length) { | ||
// processed all items, return the input `patchItem` | ||
return patchItem; | ||
} | ||
const item = items[index]; | ||
if (undefined === item || item === '') { | ||
// empty items can be ignored | ||
return createPatchPart(index + 1, target); | ||
} | ||
if (item === '[') { | ||
// next item is array index, set arrayMode to true | ||
return createPatchPart(index + 1, target); | ||
} | ||
// create object with subPart as property key, and the restuls of processing next parts as value | ||
const result = { | ||
[item]: createPatchPart(index + 1, target[item]), | ||
}; | ||
if (Array.isArray(target)) { | ||
// target in source object is array/tuple, so the patch should be object | ||
return result; | ||
} | ||
// target in source is not an array, so it patch should be an array | ||
return [result]; | ||
} | ||
return Deep.patch(source, createPatchPart(0, source)); | ||
} | ||
//# sourceMappingURL=path.js.map |
@@ -1,18 +0,2 @@ | ||
/** | ||
* Returns the same value wrapped in the `Protected` type. | ||
* @param value - the value to wrap | ||
* @note does not perform any runtime protection, it is only a utility to easily add the `Protected` | ||
* type to a value | ||
* @example | ||
* ```ts | ||
* const obj = Protected({ a: 1, b: { c: true, d: [1] } }) | ||
* obj.a = 2 // compiler error: a is readonly | ||
* obj.b.c = false // compiler error: c is readonly | ||
* obj.b.d.push(2) // compiler error: d is a readonly array | ||
* (obj as any).b.d.push(2) // will actually mutate the object | ||
* ``` | ||
*/ | ||
export function Protected(value) { | ||
return value; | ||
} | ||
export {}; | ||
//# sourceMappingURL=protected.js.map |
@@ -8,3 +8,12 @@ /** | ||
*/ | ||
export { patch, patchNested, Patch, match, Match, Path, Protected, } from './internal'; | ||
export { Tuple } from './tuple'; | ||
export type { Protected, Patch } from './internal'; | ||
export { Path, type Selector, type Match } from './internal'; | ||
export * from './deep'; | ||
import * as Deep from './deep'; | ||
export { | ||
/** | ||
* Convenience namespace offering access to most common functions used in the `@rimbu/deep` package. | ||
* These are mainly utilities to patch and match plain JavaScript objects. | ||
*/ | ||
Deep, }; |
@@ -1,4 +0,7 @@ | ||
export * from './protected'; | ||
export * from './match'; | ||
export * from './patch'; | ||
export * from './path'; | ||
export type { Protected } from './protected'; | ||
export { Path } from './path'; | ||
export { type Match } from './match'; | ||
export type { Patch } from './patch'; | ||
export type { Selector } from './selector'; | ||
import * as Deep from './deep'; | ||
export { Deep }; |
@@ -1,111 +0,106 @@ | ||
import { type IsPlainObj, type PlainObj } from '@rimbu/base'; | ||
import { IsAnyFunc, IsArray, IsPlainObj, NotIterable } from '@rimbu/base'; | ||
import type { Protected } from './internal'; | ||
import type { Tuple } from './tuple'; | ||
/** | ||
* The type to determine the allowed input values for the `match` functions. | ||
* The type to determine the allowed input values for the `match` function. | ||
* @typeparam T - the type of value to match | ||
* @typeparam C - utility type | ||
*/ | ||
export declare type Match<T> = Match.Options<T, T>; | ||
export declare type Match<T, C extends Partial<T> = Partial<T>> = Match.Entry<T, C, T, T>; | ||
export declare namespace Match { | ||
/** | ||
* The types of supported match input. | ||
* @typeparam T - the type of value to match | ||
* Determines the various allowed match types for given type `T`. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parant type | ||
* @typeparam R - the root object type | ||
*/ | ||
type Options<T, R> = Every<T, R> | Some<T, R> | None<T, R> | Single<T, R> | Match.Obj<T, R>; | ||
type Entry<T, C, P, R> = IsAnyFunc<T> extends true ? T : IsPlainObj<T> extends true ? Match.WithResult<T, P, R, Match.Obj<T, C, P, R>> : IsArray<T> extends true ? // determine allowed match values for array or tuple | ||
Match.Arr<T, C, P, R> | Match.Func<T, P, R, Match.Arr<T, C, P, R>> : Match.WithResult<T, P, R, { | ||
[K in keyof C]: C[K & keyof T]; | ||
}>; | ||
/** | ||
* The type to determine allowed matchers for objects. | ||
* @typeparam T - the type of value to match | ||
* The type that determines allowed matchers for objects. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parant type | ||
* @typeparam R - the root object type | ||
*/ | ||
type Obj<T, R> = { | ||
[K in keyof T]?: Match.ObjItem<T[K], R> | ((current: Protected<T[K]>, parent: Protected<T>, root: Protected<R>) => boolean | Match.ObjItem<T[K], R>); | ||
}; | ||
type Obj<T, C, P, R> = Match.ObjProps<T, C, R> | Match.CompoundForObj<T, C, P, R>; | ||
/** | ||
* The type to determine allowed matchers for object properties. | ||
* @typeparam T - the type of value to match | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam R - the root object type | ||
*/ | ||
type ObjItem<T, R> = IsPlainObj<T> extends true ? Match.Options<T, R> : T extends Iterable<infer U> ? T | Iterable<U> : T; | ||
type ObjProps<T, C, R> = { | ||
[K in keyof C]?: K extends keyof T ? Match.Entry<T[K], C[K], T, R> : never; | ||
}; | ||
/** | ||
* Returns a matcher that returns true if every given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* The type that determines allowed matchers for arrays/tuples. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parant type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.every({ a: 1, { c: true } } )) // => true | ||
* match(input, Match.every({ a: 1, { c: false } } )) // => false | ||
* ``` | ||
*/ | ||
function every<T, R, Q extends T = T>(...matchItems: Match.Options<Q, R>[]): Every<T, R, Q>; | ||
type Arr<T, C, P, R> = C | Match.CompoundForArr<T, C, P, R> | (Match.TupIndices<T, C, R> & { | ||
[K in Match.CompoundType]?: never; | ||
}); | ||
type WithResult<T, P, R, S> = S | Match.Func<T, P, R, S>; | ||
/** | ||
* Returns a matcher that returns true if at least one of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* Type used to determine the allowed function types. Always includes booleans. | ||
* @typeparam T - the input value type | ||
* @typeparam P - the parant type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.some({ a: 5, { c: true } } )) // => true | ||
* match(input, Match.some({ a: 5, { c: false } } )) // => false | ||
* ``` | ||
* @typeparam S - the allowed return value type | ||
*/ | ||
function some<T, R, Q extends T = T>(...matchItems: Match.Options<Q, R>[]): Some<T, R, Q>; | ||
type Func<T, P, R, S> = (current: Protected<T>, parent: Protected<P>, root: Protected<R>) => boolean | S; | ||
/** | ||
* Returns a matcher that returns true if none of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* Type used to indicate an object containing matches for tuple indices. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.none({ a: 5, { c: true } } )) // => false | ||
* match(input, Match.none({ a: 5, { c: false } } )) // => true | ||
* ``` | ||
*/ | ||
function none<T, R, Q extends T = T>(...matchItems: Match.Options<Q, R>[]): None<T, R, Q>; | ||
type TupIndices<T, C, R> = { | ||
[K in Tuple.KeysOf<C>]?: Match.Entry<T[K & keyof T], C[K], T, R>; | ||
} & NotIterable; | ||
/** | ||
* Returns a matcher that returns true if exactly one of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* Compound keys used to indicate the type of compound. | ||
*/ | ||
type CompoundType = 'every' | 'some' | 'none' | 'single'; | ||
/** | ||
* Compount matcher for objects, can only be an array staring with a compound type keyword. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.single({ a: 1, { c: true } } )) // => false | ||
* match(input, Match.single({ a: 1, { c: false } } )) // => true | ||
* ``` | ||
*/ | ||
function single<T, R, Q extends T = T>(...matchItems: Match.Options<Q, R>[]): Single<T, R, Q>; | ||
type CompoundForObj<T, C, P, R> = [ | ||
Match.CompoundType, | ||
...Match.Entry<T, C, P, R>[] | ||
]; | ||
/** | ||
* The functions that are optionally provided to a match function. | ||
* Defines an object containing exactly one `CompoundType` key, having an array of matchers. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root object type | ||
*/ | ||
type Api = typeof Match; | ||
type CompoundForArr<T, C, P, R> = { | ||
[K in CompoundType]: { | ||
[K2 in CompoundType]?: K2 extends K ? Match.Entry<T, C, P, R>[] : never; | ||
}; | ||
}[CompoundType]; | ||
/** | ||
* Utility type for collecting errors | ||
*/ | ||
type ErrorCollector = string[] | undefined; | ||
} | ||
declare class Every<T, R, Q extends T = T> { | ||
readonly matchItems: Match.Options<Q, R>[]; | ||
constructor(matchItems: Match.Options<Q, R>[]); | ||
} | ||
declare class Some<T, R, Q extends T = T> { | ||
readonly matchItems: Match.Options<Q, R>[]; | ||
constructor(matchItems: Match.Options<Q, R>[]); | ||
} | ||
declare class None<T, R, Q extends T = T> { | ||
readonly matchItems: Match.Options<Q, R>[]; | ||
constructor(matchItems: Match.Options<Q, R>[]); | ||
} | ||
declare class Single<T, R, Q extends T = T> { | ||
readonly matchItems: Match.Options<Q, R>[]; | ||
constructor(matchItems: Match.Options<Q, R>[]); | ||
} | ||
/** | ||
* Returns true if the given `value` object matches the given `matcher`, false otherwise. | ||
* @typeparam T - the input value type | ||
* @param value - the value to match (should be a plain object) | ||
* @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 errorCollector - (optional) a string array that can be passed to collect reasons why the match failed | ||
* @example | ||
@@ -116,5 +111,5 @@ * ```ts | ||
* match(input, { a: 2 }) // => false | ||
* match(input, { a: v => v > 10 }) // => false | ||
* match(input, { a: (v) => v > 10 }) // => false | ||
* match(input, { b: { c: true }}) // => true | ||
* match(input, ({ every }) => every({ a: v => v > 0 }, { 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 } }) | ||
@@ -124,3 +119,2 @@ * // => true | ||
*/ | ||
export declare function match<T>(value: T & PlainObj<T>, matcher: Match<T> | ((matchApi: Match.Api) => Match<T>)): boolean; | ||
export {}; | ||
export declare function match<T, C extends Partial<T> = Partial<T>>(source: T, matcher: Match<T, C>, errorCollector?: Match.ErrorCollector): boolean; |
@@ -1,3 +0,4 @@ | ||
import { type AnyFunc, type PlainObj } from '@rimbu/base'; | ||
import { IsAnyFunc, IsArray, IsPlainObj } from '@rimbu/base'; | ||
import type { Protected } from './internal'; | ||
import type { Tuple } from './tuple'; | ||
/** | ||
@@ -7,3 +8,3 @@ * A type to determine the allowed input type for the `patch` function. | ||
*/ | ||
export declare type Patch<T> = Patch.Entry<T, T>; | ||
export declare type Patch<T, C = T> = Patch.Entry<T, C, T, T>; | ||
export declare namespace Patch { | ||
@@ -13,72 +14,78 @@ /** | ||
* @typeparam T - the input value type | ||
* @typeparam C - a utility type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root object type | ||
*/ | ||
type Entry<T, R> = Patch.Obj<T, R> | ((patchNested: Patch.Nested) => Patch.Obj<T, R>); | ||
type Entry<T, C, P, R> = IsAnyFunc<T> extends true ? T : IsPlainObj<T> extends true ? Patch.WithResult<T, P, R, Patch.Obj<T, C, R>> : Tuple.IsTuple<T> extends true ? Patch.WithResult<T, P, R, T | Patch.Tup<T, C, R>> : IsArray<T> extends true ? Patch.WithResult<T, P, R, T> : Patch.WithResult<T, P, R, T>; | ||
/** | ||
* The object patch type, allows the user to specify keys in T that should be patched, and each given key contains either a new value or a nested patch, or a function receiving | ||
* the current value, the parent object, and the root object, and returning a new value or a nested patch. | ||
* @typeparam T - the input value type | ||
* @typeparam R - the root object type | ||
* Either result type S, or a patch function with the value type, the parent type, and the root type. | ||
* @typeparam T - the value type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root type | ||
* @typeparam S - the result type | ||
*/ | ||
type Obj<T, R> = { | ||
[K in keyof T]?: (T[K] extends AnyFunc ? never : ObjItem<T[K], R>) | ((cur: Protected<T[K]>, parent: Protected<T>, root: Protected<R>) => ObjItem<T[K], R>); | ||
type WithResult<T, P, R, S> = S | Patch.Func<T, P, R, S>; | ||
/** | ||
* A function patch type that is a function taking the current value, the parent and root values, | ||
* and returns a return value. | ||
* @typeparam T - the value type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root type | ||
* @typeparam S - the result type | ||
*/ | ||
type Func<T, P, R, S> = (current: Protected<T>, parent: Protected<P>, root: Protected<R>) => Protected<S>; | ||
/** | ||
* A type defining the allowed patch values for tuples. | ||
* @typeparam T - the input tuple type | ||
* @typeparam C - a utility type | ||
* @typeparam R - the root type | ||
*/ | ||
type Tup<T, C, R> = { | ||
[K in Tuple.KeysOf<T>]?: Patch.Entry<T[K & keyof T], C[K & keyof C], T, R>; | ||
} & NotIterable; | ||
/** | ||
* Utility type to exclude Iterable types. | ||
*/ | ||
type NotIterable = { | ||
[Symbol.iterator]?: never; | ||
}; | ||
/** | ||
* A patch object can have as update either a new value or a nested patch object | ||
* A type defining the allowed patch values for objects. | ||
* @typeparam T - the input value type | ||
* @typeparam C - a utility type | ||
* @typeparam R - the root object type | ||
*/ | ||
type ObjItem<T, R> = T | NestedObj<T, R>; | ||
type Obj<T, C, R> = T | Patch.ObjProps<T, C, R>[]; | ||
/** | ||
* The function type to create a nested Patch object. | ||
* A type defining the allowed patch values for object properties. | ||
* @typeparam T - the input value type | ||
* @typeparam C - a utility type | ||
* @typeparam R - the root object type | ||
*/ | ||
type Nested = typeof patchNested; | ||
/** | ||
* Returns a function that patches a given `value` with the given `patchItems`. | ||
* @typeparam T - the patch value type | ||
* @typeparam Q - the input value type | ||
* @param patchItems - a number of `Patch` objects that patch a given value of type T. | ||
* @example | ||
* ```ts | ||
* const items = [{ a: 1, b: 'a' }, { a: 2, b: 'b' }] | ||
* items.map(Patch.create({ a: v => v + 1 })) | ||
* // => [{ a: 2, b: 'a' }, { a: 3, b: 'b' }] | ||
* ``` | ||
*/ | ||
function create<T, Q extends T = T>(...patchItems: Patch<T & Q>[]): (value: Q & PlainObj<Q>) => Q; | ||
type ObjProps<T, C, R> = { | ||
[K in keyof C]?: K extends keyof T ? Patch.Entry<T[K], C[K], T, R> : never; | ||
}; | ||
} | ||
declare class NestedObj<T, R, Q extends T = T> { | ||
readonly patchDataItems: Patch.Obj<Q, R>[]; | ||
constructor(patchDataItems: Patch.Obj<Q, R>[]); | ||
} | ||
/** | ||
* Returns a nested patch object based on the given `patchDataItems` that work on a subpart | ||
* of a larger object to be patched. | ||
* @typeparam T - the input value type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - the patch type | ||
* @param patchDataItems - a number of `Patch` objects to be applied to the subpart of the object | ||
* @example | ||
* ```ts | ||
* patch({ a: 1, b: { c: true, d: 'a' } }, { b: patchNested({ d: 'b' }) }) | ||
* // => { a: 1, b: { c: true, d: 'b' } } | ||
* ``` | ||
*/ | ||
export declare function patchNested<T, R, Q extends T = T>(...patchDataItems: Patch.Obj<Q, R>[]): NestedObj<T, R, Q>; | ||
/** | ||
* Returns an immutably updated version of the given `value` where the given `patchItems` have been | ||
* applied to the result. | ||
* The Rimbu patch notation is as follows: | ||
* - if the target is a simple value or array, the patch can be the same type or a function returning the same type | ||
* - if the target is a tuple (array of fixed length), the patch be the same type or an object containing numeric keys with patches indicating the tuple index to patch | ||
* - if the target is an object, the patch can be the same type, or an array containing partial keys with their patches for the object | ||
* @typeparam T - the type of the value to patch | ||
* @typeparam TE - a utility type | ||
* @typeparam TT - a utility type | ||
* @param value - the input value to patch | ||
* @param patchItems - the `Patch` objects to apply to the input value | ||
* @param patchItem - the `Patch` value to apply to the input value | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* patch(input, { a: 2 }) // => { a: 2, b: { c: true, d: 'a' } } | ||
* patch(input: ($) => ({ b: $({ c: v => !v }) }) ) | ||
* patch(input, [{ a: 2 }]) // => { a: 2, b: { c: true, d: 'a' } } | ||
* patch(input, [{ b: [{ c: (v) => !v }] }] ) | ||
* // => { a: 1, b: { c: false, d: 'a' } } | ||
* patch(input: ($) => ({ a: v => v + 1, b: $({ d: 'q' }) }) ) | ||
* patch(input: [{ a: (v) => v + 1, b: [{ d: 'q' }] }] ) | ||
* // => { a: 2, b: { c: true, d: 'q' } } | ||
* ``` | ||
*/ | ||
export declare function patch<T>(value: T & PlainObj<T>, ...patchItems: Patch<T>[]): T; | ||
export {}; | ||
export declare function patch<T, TE extends T = T, TT = T>(value: T, patchItem: Patch<TE, TT>): T; |
@@ -1,53 +0,196 @@ | ||
import type { Update } from '@rimbu/common'; | ||
import type { IsPlainObj, PlainObj } from '@rimbu/base'; | ||
/** | ||
* A string representing a path into an (nested) object of type T. | ||
* @typeparam T - the object type to select in | ||
* @example | ||
* ```ts | ||
* const p: Path<{ a: { b: { c : 5 }}}> = 'a.b' | ||
* ``` | ||
*/ | ||
export declare type Path<T> = IsPlainObj<T> extends true ? { | ||
[K in string & keyof T]: `${K}` | `${K}.${Path<T[K]>}`; | ||
}[string & keyof T] : never; | ||
import type { IsAnyFunc, IsArray, IsPlainObj } from '@rimbu/base'; | ||
import { Patch } from './internal'; | ||
import type { Tuple } from './tuple'; | ||
export declare namespace Path { | ||
/** | ||
* The result type when selecting from object type T a path with type P. | ||
* A string representing a path into an (nested) object of type T. | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @example | ||
* ```ts | ||
* let r!: Path.Result<{ a: { b: { c: number } } }, 'a.b'>; | ||
* // => type of r: { c: number } | ||
* const p: Path.Get<{ a: { b: { c : 5 } } }> = 'a.b' | ||
* ``` | ||
*/ | ||
type Result<T, P extends Path<T> = Path<T>> = T extends Record<string, unknown> ? P extends `${infer Head}.${infer Rest}` ? Head extends keyof T ? Path.Result<T[Head], Rest & Path<T[Head]>> : never : P extends `${infer K}` ? T[K] : never : never; | ||
type Get<T> = Path.Internal.Generic<T, false, false, true>; | ||
/** | ||
* Returns the value resulting from selecting the given `path` in the given `source` object. | ||
* A string representing a path into an (nested) object of type T. | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @param source - the object to select in | ||
* @param path - the path into the object | ||
* @example | ||
* ```ts | ||
* console.log(Path.get({ a: { b: { c: 5 } } }), 'a.b') | ||
* // => { c: 5 } | ||
* console.log(Path.get({ a: { b: { c: 5 } } }), 'a.b.c') | ||
* // => 5 | ||
* const p: Path.Set<{ a: { b: { c : 5 } } }> = 'a.b' | ||
* ``` | ||
*/ | ||
function get<T, P extends Path<T> = Path<T>>(source: T & PlainObj<T>, path: P): Path.Result<T, P>; | ||
type Set<T> = Path.Internal.Generic<T, true, false, true>; | ||
namespace Internal { | ||
/** | ||
* Determines the allowed paths into a value of type `T`. | ||
* @typeparam T - the source type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
* @typeparam First - if true this is the root call | ||
* @note type is mapped as template literal to prevent non-string types to leak through | ||
*/ | ||
type Generic<T, Write extends boolean, Maybe extends boolean, First extends boolean = false> = `${IsAnyFunc<T> extends true ? '' : // empty string is always an option | ||
'' | Path.Internal.NonEmpty<T, Write, Maybe, First>}`; | ||
/** | ||
* Determines the allowed non-empty paths into a value of type `T`. | ||
* @typeparam T - the source type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
* @typeparam First - if true this is the root call | ||
*/ | ||
type NonEmpty<T, Write extends boolean, Maybe extends boolean, First extends boolean> = Path.Internal.IsOptional<T> extends true ? Write extends false ? Path.Internal.Generic<Exclude<T, undefined | null>, Write, true> : never : `${Path.Internal.Separator<First, Maybe, IsArray<T>>}${Path.Internal.NonOptional<T, Write, Maybe>}`; | ||
/** | ||
* Determines the allowed paths into a non-optional value of type `T`. | ||
* @typeparam T - the source type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
* @typeparam First - if true this is the root call | ||
*/ | ||
type NonOptional<T, Write extends boolean, Maybe extends boolean> = Tuple.IsTuple<T> extends true ? Path.Internal.Tup<T, Write, Maybe> : T extends readonly any[] ? Write extends false ? Path.Internal.Arr<T> : never : IsPlainObj<T> extends true ? Path.Internal.Obj<T, Write, Maybe> : never; | ||
/** | ||
* Determines the allowed paths for a tuple. Since tuples have fixed types, they do not | ||
* need to be optional, in contrast to arrays. | ||
* @typeparam T - the input tuple type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
*/ | ||
type Tup<T, Write extends boolean, Maybe extends boolean> = { | ||
[K in Tuple.KeysOf<T>]: `[${K}]${Path.Internal.Generic<T[K], Write, Maybe>}`; | ||
}[Tuple.KeysOf<T>]; | ||
/** | ||
* Determines the allowed paths for an array. | ||
* @typeparam T - the input array type | ||
*/ | ||
type Arr<T extends readonly any[]> = `[${number}]${Path.Internal.Generic<T[number], false, true>}`; | ||
/** | ||
* Determines the allowed paths for an object. | ||
* @typeparam T - the input object type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
*/ | ||
type Obj<T, Write extends boolean, Maybe extends boolean> = { | ||
[K in keyof T]: `${K & string}${Path.Internal.Generic<T[K], Write, Write extends true ? false : Path.Internal.IsOptional<T[K], true, Maybe>>}`; | ||
}[keyof T]; | ||
/** | ||
* Determines the allowed path part seperator based on the input types. | ||
* @typeparam First - if true, this is the first call | ||
* @typeparam Maybe - if true, the value is optional | ||
* @typeparam IsArray - if true, the value is an array | ||
*/ | ||
type Separator<First extends boolean, Maybe extends boolean, IsArray extends boolean> = Maybe extends true ? First extends true ? never : '?.' : First extends true ? '' : IsArray extends true ? '' : '.'; | ||
/** | ||
* Determines whether the given type `T` is optional, that is, whether it can be null or undefined. | ||
* @typeparam T - the input type | ||
* @typeparam True - the value to return if `T` is optional | ||
* @typeparam False - the value to return if `T` is mandatory | ||
*/ | ||
type IsOptional<T, True = true, False = false> = undefined extends T ? True : null extends T ? True : False; | ||
/** | ||
* Returns type `T` if `Maybe` is false, `T | undefined` otherwise. | ||
* @typeparam T - the input type | ||
* @typeparam Maybe - if true, the return type value should be optional | ||
*/ | ||
type MaybeValue<T, Maybe extends boolean> = Maybe extends true ? T | undefined : T; | ||
/** | ||
* Utility type to only add non-empty string types to a string array. | ||
* @typeparma A - the input string array | ||
* @typeparam T - the string value to optionally add | ||
*/ | ||
type AppendIfNotEmpty<A extends string[], T extends string> = T extends '' ? A : [ | ||
...A, | ||
T | ||
]; | ||
} | ||
/** | ||
* Sets the value at the given path in the source to the given value. | ||
* @param source - the object to update | ||
* @param path - the path in the object to update | ||
* @param value - the new value to set at the given position | ||
* The result type when selecting from object type T a path with type P. | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @example | ||
* ```ts | ||
* console.log(Path.update({ a: { b: { c: 5 } } }, 'a.b.c', v => v + 5) | ||
* // => { a: { b: { c: 6 } } } | ||
* let r!: Path.Result<{ a: { b: { c: number } } }, 'a.b'>; | ||
* // => type of r: { c: number } | ||
* ``` | ||
*/ | ||
function update<T, P extends Path<T> = Path<T>>(source: T & PlainObj<T>, path: P, value: Update<Path.Result<T, P>>): T; | ||
type Result<T, P extends string> = Path.Result.For<T, Path.Result.Tokenize<P>, false>; | ||
namespace Result { | ||
/** | ||
* Determines the result type for an array of tokens representing subpaths in type `T`. | ||
* @typeparam T - the current source type | ||
* @typeparam Tokens - an array of elements indicating a path into the source type | ||
* @typeparam Maybe - if true indicates that the path may be undefined | ||
*/ | ||
type For<T, Tokens, Maybe extends boolean = Path.Internal.IsOptional<T>> = Tokens extends [] ? Path.Internal.MaybeValue<T, Maybe> : Path.Internal.IsOptional<T> extends true ? Path.Result.For<Exclude<T, undefined | null>, Tokens, Maybe> : Tokens extends ['?.', infer Key, ...infer Rest] ? Path.Result.For<Path.Result.Part<T, Key, Maybe>, Rest, true> : Tokens extends ['.', infer Key, ...infer Rest] ? Path.Result.For<Path.Result.Part<T, Key, false>, Rest, Maybe> : Tokens extends [infer Key, ...infer Rest] ? Path.Result.For<Path.Result.Part<T, Key, false>, Rest, Maybe> : never; | ||
/** | ||
* Determines the result of getting the property/index `K` from type `T`, taking into | ||
* account that the value may be optional. | ||
* @typeparam T - the current source type | ||
* @typeparam K - the key to get from the source type | ||
* @typeparam Maybe - if true indicates that the path may be undefined | ||
*/ | ||
type Part<T, K, Maybe extends boolean> = IsArray<T> extends true ? Path.Internal.MaybeValue<T[K & keyof T], Tuple.IsTuple<T> extends true ? Maybe : true> : Path.Internal.MaybeValue<T[K & keyof T], Maybe>; | ||
/** | ||
* Converts a path string into separate tokens in a string array. | ||
* @typeparam P - the literal string path type | ||
* @typeparam Token - the token currently being produced | ||
* @typeparam Res - the resulting literal string token array | ||
*/ | ||
type Tokenize<P extends string, Token extends string = '', Res extends string[] = []> = P extends '' ? Path.Internal.AppendIfNotEmpty<Res, Token> : P extends `[${infer Index}]${infer Rest}` ? Tokenize<Rest, '', [ | ||
...Path.Internal.AppendIfNotEmpty<Res, Token>, | ||
Index | ||
]> : P extends `?.${infer Rest}` ? Tokenize<Rest, '', [ | ||
...Path.Internal.AppendIfNotEmpty<Res, Token>, | ||
'?.' | ||
]> : P extends `.${infer Rest}` ? Tokenize<Rest, '', [...Path.Internal.AppendIfNotEmpty<Res, Token>, '.']> : P extends `${infer First}${infer Rest}` ? Tokenize<Rest, `${Token}${First}`, Res> : never; | ||
} | ||
/** | ||
* Regular expression used to split a path string into tokens. | ||
*/ | ||
const stringSplitRegex: RegExp; | ||
/** | ||
* The allowed values of a split path. | ||
*/ | ||
type StringSplit = (string | number | undefined)[]; | ||
/** | ||
* Return the given `path` string split into an array of subpaths. | ||
* @param path - the input string path | ||
*/ | ||
function stringSplit(path: string): Path.StringSplit; | ||
} | ||
/** | ||
* Returns the value resulting from selecting the given `path` in the given `source` object. | ||
* It supports optional chaining for nullable values or values that may be undefined, and also | ||
* for accessing objects inside an array. | ||
* There is currently no support for forcing non-null (the `!` operator). | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @param source - the object to select in | ||
* @param path - the path into the object | ||
* @example | ||
* ```ts | ||
* const value = { a: { b: { c: [{ d: 5 }, { d: 6 }] } } } | ||
* Deep.getAt(value, 'a.b'); | ||
* // => { c: 5 } | ||
* Deep.getAt(value, 'a.b.c'); | ||
* // => [{ d: 5 }, { d: 5 }] | ||
* Deep.getAt(value, 'a.b.c[1]'); | ||
* // => { d: 6 } | ||
* Deep.getAt(value, 'a.b.c[1]?.d'); | ||
* // => 6 | ||
* ``` | ||
*/ | ||
export declare function getAt<T, P extends Path.Get<T>>(source: T, path: P): Path.Result<T, P>; | ||
/** | ||
* Patches the value at the given path in the source to the given value. | ||
* Because the path to update must exist in the `source` object, optional | ||
* chaining and array indexing is not allowed. | ||
* @param source - the object to update | ||
* @param path - the path in the object to update | ||
* @param patchItem - the patch for the value at the given path | ||
* @example | ||
* ```ts | ||
* const value = { a: { b: { c: 5 } } }; | ||
* Deep.patchAt(value, 'a.b.c', v => v + 5); | ||
* // => { a: { b: { c: 6 } } } | ||
* ``` | ||
*/ | ||
export declare function patchAt<T, P extends Path.Set<T>, C = Path.Result<T, P>>(source: T, path: P, patchItem: Patch<Path.Result<T, P>, C>): T; |
@@ -15,19 +15,4 @@ import type { IsAny, IsPlainObj } from '@rimbu/base'; | ||
readonly [K in keyof A]: Protected<A[K]>; | ||
} : T extends Map<infer K, infer V> ? Map<Protected<K>, Protected<V>> : T extends Set<infer E> ? Set<Protected<E>> : T extends Promise<infer E> ? Promise<Protected<E>> : IsPlainObj<T> extends true ? { | ||
} : T extends Map<infer K, infer V> ? Omit<Map<Protected<K>, Protected<V>>, 'clear' | 'delete' | 'set'> : T extends Set<infer E> ? Omit<Set<Protected<E>>, 'add' | 'clear' | 'delete'> : T extends Promise<infer E> ? Promise<Protected<E>> : IsPlainObj<T> extends true ? { | ||
readonly [K in keyof T]: Protected<T[K]>; | ||
} : T; | ||
/** | ||
* Returns the same value wrapped in the `Protected` type. | ||
* @param value - the value to wrap | ||
* @note does not perform any runtime protection, it is only a utility to easily add the `Protected` | ||
* type to a value | ||
* @example | ||
* ```ts | ||
* const obj = Protected({ a: 1, b: { c: true, d: [1] } }) | ||
* obj.a = 2 // compiler error: a is readonly | ||
* obj.b.c = false // compiler error: c is readonly | ||
* obj.b.d.push(2) // compiler error: d is a readonly array | ||
* (obj as any).b.d.push(2) // will actually mutate the object | ||
* ``` | ||
*/ | ||
export declare function Protected<T>(value: T): Protected<T>; |
@@ -15,3 +15,13 @@ import type { Update } from '@rimbu/common'; | ||
type Source = readonly unknown[]; | ||
type IsTuple<T> = T extends { | ||
length: infer L; | ||
} ? 0 extends L ? false : true : false; | ||
/** | ||
* Returns the indices/keys that are in a tuple. | ||
* @typeparam T - the input tuple type | ||
*/ | ||
type KeysOf<T> = { | ||
[K in keyof T]: K; | ||
}[keyof T & number]; | ||
/** | ||
* Convenience method to type Tuple types | ||
@@ -18,0 +28,0 @@ * @param values - the values of the tuple |
{ | ||
"name": "@rimbu/deep", | ||
"version": "0.11.3", | ||
"version": "0.12.0", | ||
"description": "Tools to use handle plain JS objects as immutable objects", | ||
@@ -60,3 +60,3 @@ "keywords": [ | ||
"dependencies": { | ||
"@rimbu/base": "^0.9.5", | ||
"@rimbu/base": "^0.10.0", | ||
"@rimbu/common": "^0.10.3", | ||
@@ -72,3 +72,3 @@ "tslib": "^2.4.0" | ||
}, | ||
"gitHead": "c4009664c4e15f367e963d198cacd7c5fc182a4d" | ||
"gitHead": "22ae1cadeb885e7fd30a23bd05c001cda3141e86" | ||
} |
@@ -9,12 +9,16 @@ /** | ||
export { Tuple } from './tuple'; | ||
export type { Protected, Patch } from './internal'; | ||
export { Path, type Selector, type Match } from './internal'; | ||
export * from './deep'; | ||
import * as Deep from './deep'; | ||
export { | ||
patch, | ||
patchNested, | ||
Patch, | ||
match, | ||
Match, | ||
Path, | ||
Protected, | ||
} from './internal'; | ||
export { Tuple } from './tuple'; | ||
/** | ||
* Convenience namespace offering access to most common functions used in the `@rimbu/deep` package. | ||
* These are mainly utilities to patch and match plain JavaScript objects. | ||
*/ | ||
Deep, | ||
}; |
@@ -1,5 +0,8 @@ | ||
export * from './protected'; | ||
export type { Protected } from './protected'; | ||
export { Path } from './path'; | ||
export { type Match } from './match'; | ||
export type { Patch } from './patch'; | ||
export type { Selector } from './selector'; | ||
export * from './match'; | ||
export * from './patch'; | ||
export * from './path'; | ||
import * as Deep from './deep'; | ||
export { Deep }; |
606
src/match.ts
import { | ||
RimbuError, | ||
type IsPlainObj, | ||
type PlainObj, | ||
IsAnyFunc, | ||
IsArray, | ||
isPlainObj, | ||
isIterable, | ||
IsPlainObj, | ||
NotIterable, | ||
} from '@rimbu/base'; | ||
import type { Protected } from './internal'; | ||
import type { Tuple } from './tuple'; | ||
/** | ||
* The type to determine the allowed input values for the `match` functions. | ||
* The type to determine the allowed input values for the `match` function. | ||
* @typeparam T - the type of value to match | ||
* @typeparam C - utility type | ||
*/ | ||
export type Match<T> = Match.Options<T, T>; | ||
export type Match<T, C extends Partial<T> = Partial<T>> = Match.Entry< | ||
T, | ||
C, | ||
T, | ||
T | ||
>; | ||
export namespace Match { | ||
/** | ||
* The types of supported match input. | ||
* @typeparam T - the type of value to match | ||
* Determines the various allowed match types for given type `T`. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parant type | ||
* @typeparam R - the root object type | ||
*/ | ||
export type Options<T, R> = | ||
| Every<T, R> | ||
| Some<T, R> | ||
| None<T, R> | ||
| Single<T, R> | ||
| Match.Obj<T, R>; | ||
export type Entry<T, C, P, R> = IsAnyFunc<T> extends true | ||
? // function can only be directly matched | ||
T | ||
: IsPlainObj<T> extends true | ||
? // determine allowed match values for object | ||
Match.WithResult<T, P, R, Match.Obj<T, C, P, R>> | ||
: IsArray<T> extends true | ||
? // determine allowed match values for array or tuple | ||
Match.Arr<T, C, P, R> | Match.Func<T, P, R, Match.Arr<T, C, P, R>> | ||
: // only accept values with same interface | ||
Match.WithResult<T, P, R, { [K in keyof C]: C[K & keyof T] }>; | ||
/** | ||
* The type to determine allowed matchers for objects. | ||
* @typeparam T - the type of value to match | ||
* The type that determines allowed matchers for objects. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parant type | ||
* @typeparam R - the root object type | ||
*/ | ||
export type Obj<T, R> = { | ||
[K in keyof T]?: | ||
| Match.ObjItem<T[K], R> | ||
| (( | ||
current: Protected<T[K]>, | ||
parent: Protected<T>, | ||
root: Protected<R> | ||
) => boolean | Match.ObjItem<T[K], R>); | ||
}; | ||
export type Obj<T, C, P, R> = | ||
| Match.ObjProps<T, C, R> | ||
| Match.CompoundForObj<T, C, P, R>; | ||
/** | ||
* The type to determine allowed matchers for object properties. | ||
* @typeparam T - the type of value to match | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam R - the root object type | ||
*/ | ||
export type ObjItem<T, R> = IsPlainObj<T> extends true | ||
? Match.Options<T, R> | ||
: T extends Iterable<infer U> | ||
? T | Iterable<U> | ||
: T; | ||
export type ObjProps<T, C, R> = { | ||
[K in keyof C]?: K extends keyof T ? Match.Entry<T[K], C[K], T, R> : never; | ||
}; | ||
/** | ||
* Returns a matcher that returns true if every given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* The type that determines allowed matchers for arrays/tuples. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parant type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.every({ a: 1, { c: true } } )) // => true | ||
* match(input, Match.every({ a: 1, { c: false } } )) // => false | ||
* ``` | ||
*/ | ||
export function every<T, R, Q extends T = T>( | ||
...matchItems: Match.Options<Q, R>[] | ||
): Every<T, R, Q> { | ||
return new Every(matchItems); | ||
} | ||
export type Arr<T, C, P, R> = | ||
| C | ||
| Match.CompoundForArr<T, C, P, R> | ||
| (Match.TupIndices<T, C, R> & { [K in Match.CompoundType]?: never }); | ||
export type WithResult<T, P, R, S> = S | Match.Func<T, P, R, S>; | ||
/** | ||
* Returns a matcher that returns true if at least one of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* Type used to determine the allowed function types. Always includes booleans. | ||
* @typeparam T - the input value type | ||
* @typeparam P - the parant type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.some({ a: 5, { c: true } } )) // => true | ||
* match(input, Match.some({ a: 5, { c: false } } )) // => false | ||
* ``` | ||
* @typeparam S - the allowed return value type | ||
*/ | ||
export function some<T, R, Q extends T = T>( | ||
...matchItems: Match.Options<Q, R>[] | ||
): Some<T, R, Q> { | ||
return new Some(matchItems); | ||
} | ||
export type Func<T, P, R, S> = ( | ||
current: Protected<T>, | ||
parent: Protected<P>, | ||
root: Protected<R> | ||
) => boolean | S; | ||
/** | ||
* Returns a matcher that returns true if none of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* Type used to indicate an object containing matches for tuple indices. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.none({ a: 5, { c: true } } )) // => false | ||
* match(input, Match.none({ a: 5, { c: false } } )) // => true | ||
* ``` | ||
*/ | ||
export function none<T, R, Q extends T = T>( | ||
...matchItems: Match.Options<Q, R>[] | ||
): None<T, R, Q> { | ||
return new None(matchItems); | ||
} | ||
export type TupIndices<T, C, R> = { | ||
[K in Tuple.KeysOf<C>]?: Match.Entry<T[K & keyof T], C[K], T, R>; | ||
} & NotIterable; | ||
/** | ||
* Returns a matcher that returns true if exactly one of given `matchItem` matches the given value. | ||
* @typeparam T - the type of value to match | ||
* Compound keys used to indicate the type of compound. | ||
*/ | ||
export type CompoundType = 'every' | 'some' | 'none' | 'single'; | ||
/** | ||
* Compount matcher for objects, can only be an array staring with a compound type keyword. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - a utility type for the matcher | ||
* @param matchItems - the match specifications to test | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* match(input, Match.single({ a: 1, { c: true } } )) // => false | ||
* match(input, Match.single({ a: 1, { c: false } } )) // => true | ||
* ``` | ||
*/ | ||
export function single<T, R, Q extends T = T>( | ||
...matchItems: Match.Options<Q, R>[] | ||
): Single<T, R, Q> { | ||
return new Single(matchItems); | ||
} | ||
export type CompoundForObj<T, C, P, R> = [ | ||
Match.CompoundType, | ||
...Match.Entry<T, C, P, R>[] | ||
]; | ||
/** | ||
* The functions that are optionally provided to a match function. | ||
* Defines an object containing exactly one `CompoundType` key, having an array of matchers. | ||
* @typeparam T - the input value type | ||
* @typeparam C - utility type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root object type | ||
*/ | ||
export type Api = typeof Match; | ||
} | ||
export type CompoundForArr<T, C, P, R> = { | ||
[K in CompoundType]: { | ||
[K2 in CompoundType]?: K2 extends K ? Match.Entry<T, C, P, R>[] : never; | ||
}; | ||
}[CompoundType]; | ||
class Every<T, R, Q extends T = T> { | ||
constructor(readonly matchItems: Match.Options<Q, R>[]) {} | ||
/** | ||
* Utility type for collecting errors | ||
*/ | ||
export type ErrorCollector = string[] | undefined; | ||
} | ||
class Some<T, R, Q extends T = T> { | ||
constructor(readonly matchItems: Match.Options<Q, R>[]) {} | ||
} | ||
class None<T, R, Q extends T = T> { | ||
constructor(readonly matchItems: Match.Options<Q, R>[]) {} | ||
} | ||
class Single<T, R, Q extends T = T> { | ||
constructor(readonly matchItems: Match.Options<Q, R>[]) {} | ||
} | ||
/** | ||
* Returns true if the given `value` object matches the given `matcher`, false otherwise. | ||
* @typeparam T - the input value type | ||
* @param value - the value to match (should be a plain object) | ||
* @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 errorCollector - (optional) a string array that can be passed to collect reasons why the match failed | ||
* @example | ||
@@ -161,5 +149,5 @@ * ```ts | ||
* match(input, { a: 2 }) // => false | ||
* match(input, { a: v => v > 10 }) // => false | ||
* match(input, { a: (v) => v > 10 }) // => false | ||
* match(input, { b: { c: true }}) // => true | ||
* match(input, ({ every }) => every({ a: v => v > 0 }, { 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 } }) | ||
@@ -169,49 +157,124 @@ * // => true | ||
*/ | ||
export function match<T>( | ||
value: T & PlainObj<T>, | ||
matcher: Match<T> | ((matchApi: Match.Api) => Match<T>) | ||
export function match<T, C extends Partial<T> = Partial<T>>( | ||
source: T, | ||
matcher: Match<T, C>, | ||
errorCollector: Match.ErrorCollector = undefined | ||
): boolean { | ||
if (matcher instanceof Function) { | ||
return matchOptions(value, value, matcher(Match)); | ||
} | ||
return matchOptions(value, value, matcher); | ||
return matchEntry(source, source, source, matcher as any, errorCollector); | ||
} | ||
function matchOptions<T, R>( | ||
value: T, | ||
/** | ||
* Match a generic match entry against the given source. | ||
*/ | ||
function matchEntry<T, C, P, R>( | ||
source: T, | ||
parent: P, | ||
root: R, | ||
matcher: Match.Options<T, R> | ||
matcher: Match.Entry<T, C, P, R>, | ||
errorCollector: Match.ErrorCollector | ||
): boolean { | ||
if (matcher instanceof Every) { | ||
let i = -1; | ||
const { matchItems } = matcher; | ||
const len = matchItems.length; | ||
if (Object.is(source, matcher)) { | ||
// value and target are exactly the same, always will be true | ||
return true; | ||
} | ||
while (++i < len) { | ||
if (!matchOptions(value, root, matchItems[i])) { | ||
return false; | ||
} | ||
if (matcher === null || matcher === undefined) { | ||
// these matchers can only be direct matches, and previously it was determined that | ||
// they are not equal | ||
errorCollector?.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) { | ||
errorCollector?.push( | ||
`both value and matcher are functions, but they do not have the same reference` | ||
); | ||
} | ||
return true; | ||
return result; | ||
} | ||
if (matcher instanceof Some) { | ||
let i = -1; | ||
const { matchItems } = matcher; | ||
const len = matchItems.length; | ||
while (++i < len) { | ||
if (matchOptions(value, root, matchItems[i])) { | ||
return true; | ||
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) { | ||
errorCollector?.push( | ||
`function matcher returned false for value ${JSON.stringify(source)}` | ||
); | ||
} | ||
return matcherResult; | ||
} | ||
return false; | ||
// function resulted in a value that needs to be further matched | ||
return matchEntry(source, parent, root, matcherResult, errorCollector); | ||
} | ||
if (matcher instanceof None) { | ||
let i = -1; | ||
const { matchItems } = matcher; | ||
const len = matchItems.length; | ||
while (++i < len) { | ||
if (matchOptions(value, root, matchItems[i])) { | ||
if (isPlainObj(source)) { | ||
// source ia a plain object, can be partially matched | ||
return matchPlainObj(source, parent, root, matcher as any, errorCollector); | ||
} | ||
if (Array.isArray(source)) { | ||
// source is an array | ||
return matchArr(source, root, matcher as any, errorCollector); | ||
} | ||
// already determined above that the source and matcher are not equal | ||
errorCollector?.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<T extends any[], C, P, R>( | ||
source: T, | ||
root: R, | ||
matcher: Match.Arr<T, C, P, R>, | ||
errorCollector: Match.ErrorCollector | ||
): boolean { | ||
if (Array.isArray(matcher)) { | ||
// directly compare array contents | ||
const length = source.length; | ||
if (length !== matcher.length) { | ||
// if lengths not equal, arrays are not equal | ||
errorCollector?.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 (!Object.is(source[index], matcher[index])) { | ||
// item did not match, return false | ||
errorCollector?.push( | ||
`index ${index} does not match with value ${JSON.stringify( | ||
source[index] | ||
)} and matcher ${matcher[index]}` | ||
); | ||
return false; | ||
@@ -221,101 +284,222 @@ } | ||
// all items are equal | ||
return true; | ||
} | ||
if (matcher instanceof Single) { | ||
let i = -1; | ||
const { matchItems } = matcher; | ||
const len = matchItems.length; | ||
let matched = false; | ||
while (++i < len) { | ||
if (matchOptions(value, root, matchItems[i])) { | ||
if (matched) { | ||
return false; | ||
} | ||
// matcher is plain object with index keys | ||
matched = true; | ||
} | ||
for (const index in matcher as any) { | ||
const matcherAtIndex = (matcher as any)[index]; | ||
if (!(index in source)) { | ||
// source does not have item at given index | ||
errorCollector?.push( | ||
`index ${index} does not exist in source ${JSON.stringify( | ||
source | ||
)} but should match matcher ${JSON.stringify(matcherAtIndex)}` | ||
); | ||
return false; | ||
} | ||
return matched; | ||
} | ||
// match the source item at the given index | ||
const result = matchEntry( | ||
(source as any)[index], | ||
source, | ||
root, | ||
matcherAtIndex, | ||
errorCollector | ||
); | ||
if (isPlainObj(matcher)) { | ||
return matchRecord(value, root, matcher); | ||
if (!result) { | ||
// item did not match | ||
errorCollector?.push( | ||
`index ${index} does not match with value ${JSON.stringify( | ||
(source as any)[index] | ||
)} and matcher ${JSON.stringify(matcherAtIndex)}` | ||
); | ||
return false; | ||
} | ||
} | ||
return Object.is(value, matcher); | ||
// all items match | ||
return true; | ||
} | ||
function matchRecord<T, R>( | ||
value: T, | ||
/** | ||
* Match an object matcher against the given source. | ||
*/ | ||
function matchPlainObj<T, C, P, R>( | ||
source: T, | ||
parent: P, | ||
root: R, | ||
matcher: Match.Obj<T, R> | ||
matcher: Match.Obj<T, C, P, R>, | ||
errorCollector: Match.ErrorCollector | ||
): boolean { | ||
if (!isPlainObj(matcher)) { | ||
RimbuError.throwInvalidUsageError( | ||
'match: to prevent accidental errors, match only supports plain objects as input.' | ||
); | ||
if (Array.isArray(matcher)) { | ||
// the matcher is of compound type | ||
return matchCompound(source, parent, root, matcher as any, errorCollector); | ||
} | ||
// partial object props matcher | ||
for (const key in matcher) { | ||
if (!(key in value)) return false; | ||
if (!(key in source)) { | ||
// the source does not have the given key | ||
const matchValue = matcher[key]; | ||
const target = value[key]; | ||
errorCollector?.push( | ||
`key ${key} is specified in matcher but not present in value ${JSON.stringify( | ||
source | ||
)}` | ||
); | ||
if (matchValue instanceof Function) { | ||
if (target instanceof Function && Object.is(target, matchValue)) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
const result = matchValue(target, value, root); | ||
// match the source value at the given key with the matcher at given key | ||
const result = matchEntry( | ||
(source as any)[key], | ||
source, | ||
root, | ||
matcher[key], | ||
errorCollector | ||
); | ||
if (typeof result === 'boolean') { | ||
if (result) { | ||
continue; | ||
} | ||
return false; | ||
} | ||
if (!matchRecordItem(target, root, result)) { | ||
return false; | ||
} | ||
} else { | ||
if (!matchRecordItem(target, root, matchValue as any)) { | ||
return false; | ||
} | ||
if (!result) { | ||
errorCollector?.push( | ||
`key ${key} does not match in value ${JSON.stringify( | ||
(source as any)[key] | ||
)} with matcher ${JSON.stringify(matcher[key])}` | ||
); | ||
return false; | ||
} | ||
} | ||
// all properties match | ||
return true; | ||
} | ||
function matchRecordItem<T, R>( | ||
value: T, | ||
/** | ||
* Match a compound matcher against the given source. | ||
*/ | ||
function matchCompound<T, C, P, R>( | ||
source: T, | ||
parent: P, | ||
root: R, | ||
matcher: Match.ObjItem<T, R> | ||
compound: [Match.CompoundType, ...Match.Entry<T, C, P, R>[]], | ||
errorCollector: string[] | undefined | ||
): boolean { | ||
if (isIterable(matcher) && isIterable(value)) { | ||
const it1 = (value as any)[Symbol.iterator]() as Iterator<unknown>; | ||
const it2 = (matcher as any)[Symbol.iterator]() as Iterator<unknown>; | ||
// first item indicates compound match type | ||
const matchType = compound[0]; | ||
while (true) { | ||
const v1 = it1.next(); | ||
const v2 = it2.next(); | ||
const length = compound.length; | ||
if (v1.done !== v2.done || v1.value !== v2.value) { | ||
return false; | ||
// start at index 1 | ||
let index = 0; | ||
type Entry = Match.Entry<T, C, P, R>; | ||
switch (matchType) { | ||
case 'every': { | ||
while (++index < length) { | ||
// if any item does not match, return false | ||
const result = matchEntry( | ||
source, | ||
parent, | ||
root, | ||
compound[index] as Entry, | ||
errorCollector | ||
); | ||
if (!result) { | ||
errorCollector?.push( | ||
`in compound "every": match at index ${index} failed` | ||
); | ||
return false; | ||
} | ||
} | ||
if (v1.done) { | ||
return true; | ||
return true; | ||
} | ||
case 'none': { | ||
// if any item matches, return false | ||
while (++index < length) { | ||
const result = matchEntry( | ||
source, | ||
parent, | ||
root, | ||
compound[index] as Entry, | ||
errorCollector | ||
); | ||
if (result) { | ||
errorCollector?.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] as Entry, | ||
errorCollector | ||
); | ||
if (result) { | ||
if (onePassed) { | ||
errorCollector?.push( | ||
`in compound "single": multiple matches succeeded` | ||
); | ||
return false; | ||
} | ||
onePassed = true; | ||
} | ||
} | ||
if (!onePassed) { | ||
errorCollector?.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] as Entry, | ||
errorCollector | ||
); | ||
if (result) { | ||
return true; | ||
} | ||
} | ||
errorCollector?.push(`in compound "some": no matches succeeded`); | ||
return false; | ||
} | ||
} | ||
if (isPlainObj(value)) { | ||
return matchOptions(value, root, matcher as any); | ||
} | ||
return Object.is(value, matcher); | ||
} |
347
src/patch.ts
@@ -1,9 +0,5 @@ | ||
import { | ||
RimbuError, | ||
type AnyFunc, | ||
isPlainObj, | ||
type PlainObj, | ||
} from '@rimbu/base'; | ||
import { IsAnyFunc, IsArray, isPlainObj, IsPlainObj } from '@rimbu/base'; | ||
import type { Protected } from './internal'; | ||
import type { Tuple } from './tuple'; | ||
@@ -14,3 +10,3 @@ /** | ||
*/ | ||
export type Patch<T> = Patch.Entry<T, T>; | ||
export type Patch<T, C = T> = Patch.Entry<T, C, T, T>; | ||
@@ -21,76 +17,73 @@ export namespace Patch { | ||
* @typeparam T - the input value type | ||
* @typeparam C - a utility type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root object type | ||
*/ | ||
export type Entry<T, R> = | ||
| Patch.Obj<T, R> | ||
| ((patchNested: Patch.Nested) => Patch.Obj<T, R>); | ||
export type Entry<T, C, P, R> = IsAnyFunc<T> extends true | ||
? T | ||
: IsPlainObj<T> extends true | ||
? Patch.WithResult<T, P, R, Patch.Obj<T, C, R>> | ||
: Tuple.IsTuple<T> extends true | ||
? Patch.WithResult<T, P, R, T | Patch.Tup<T, C, R>> | ||
: IsArray<T> extends true | ||
? Patch.WithResult<T, P, R, T> | ||
: Patch.WithResult<T, P, R, T>; | ||
/** | ||
* The object patch type, allows the user to specify keys in T that should be patched, and each given key contains either a new value or a nested patch, or a function receiving | ||
* the current value, the parent object, and the root object, and returning a new value or a nested patch. | ||
* @typeparam T - the input value type | ||
* @typeparam R - the root object type | ||
* Either result type S, or a patch function with the value type, the parent type, and the root type. | ||
* @typeparam T - the value type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root type | ||
* @typeparam S - the result type | ||
*/ | ||
export type Obj<T, R> = { | ||
[K in keyof T]?: | ||
| (T[K] extends AnyFunc ? never : ObjItem<T[K], R>) | ||
| (( | ||
cur: Protected<T[K]>, | ||
parent: Protected<T>, | ||
root: Protected<R> | ||
) => ObjItem<T[K], R>); | ||
}; | ||
export type WithResult<T, P, R, S> = S | Patch.Func<T, P, R, S>; | ||
/** | ||
* A patch object can have as update either a new value or a nested patch object | ||
* @typeparam T - the input value type | ||
* @typeparam R - the root object type | ||
* A function patch type that is a function taking the current value, the parent and root values, | ||
* and returns a return value. | ||
* @typeparam T - the value type | ||
* @typeparam P - the parent type | ||
* @typeparam R - the root type | ||
* @typeparam S - the result type | ||
*/ | ||
export type ObjItem<T, R> = T | NestedObj<T, R>; | ||
export type Func<T, P, R, S> = ( | ||
current: Protected<T>, | ||
parent: Protected<P>, | ||
root: Protected<R> | ||
) => Protected<S>; | ||
/** | ||
* The function type to create a nested Patch object. | ||
* A type defining the allowed patch values for tuples. | ||
* @typeparam T - the input tuple type | ||
* @typeparam C - a utility type | ||
* @typeparam R - the root type | ||
*/ | ||
export type Nested = typeof patchNested; | ||
export type Tup<T, C, R> = { | ||
[K in Tuple.KeysOf<T>]?: Patch.Entry<T[K & keyof T], C[K & keyof C], T, R>; | ||
} & NotIterable; | ||
/** | ||
* Returns a function that patches a given `value` with the given `patchItems`. | ||
* @typeparam T - the patch value type | ||
* @typeparam Q - the input value type | ||
* @param patchItems - a number of `Patch` objects that patch a given value of type T. | ||
* @example | ||
* ```ts | ||
* const items = [{ a: 1, b: 'a' }, { a: 2, b: 'b' }] | ||
* items.map(Patch.create({ a: v => v + 1 })) | ||
* // => [{ a: 2, b: 'a' }, { a: 3, b: 'b' }] | ||
* ``` | ||
* Utility type to exclude Iterable types. | ||
*/ | ||
export function create<T, Q extends T = T>( | ||
...patchItems: Patch<T & Q>[] | ||
): (value: Q & PlainObj<Q>) => Q { | ||
return (value) => patch<Q>(value, ...patchItems); | ||
} | ||
} | ||
export type NotIterable = { | ||
[Symbol.iterator]?: never; | ||
}; | ||
class NestedObj<T, R, Q extends T = T> { | ||
constructor(readonly patchDataItems: Patch.Obj<Q, R>[]) {} | ||
} | ||
/** | ||
* A type defining the allowed patch values for objects. | ||
* @typeparam T - the input value type | ||
* @typeparam C - a utility type | ||
* @typeparam R - the root object type | ||
*/ | ||
export type Obj<T, C, R> = T | Patch.ObjProps<T, C, R>[]; | ||
/** | ||
* Returns a nested patch object based on the given `patchDataItems` that work on a subpart | ||
* of a larger object to be patched. | ||
* @typeparam T - the input value type | ||
* @typeparam R - the root object type | ||
* @typeparam Q - the patch type | ||
* @param patchDataItems - a number of `Patch` objects to be applied to the subpart of the object | ||
* @example | ||
* ```ts | ||
* patch({ a: 1, b: { c: true, d: 'a' } }, { b: patchNested({ d: 'b' }) }) | ||
* // => { a: 1, b: { c: true, d: 'b' } } | ||
* ``` | ||
*/ | ||
export function patchNested<T, R, Q extends T = T>( | ||
...patchDataItems: Patch.Obj<Q, R>[] | ||
): NestedObj<T, R, Q> { | ||
return new NestedObj(patchDataItems); | ||
/** | ||
* A type defining the allowed patch values for object properties. | ||
* @typeparam T - the input value type | ||
* @typeparam C - a utility type | ||
* @typeparam R - the root object type | ||
*/ | ||
export type ObjProps<T, C, R> = { | ||
[K in keyof C]?: K extends keyof T ? Patch.Entry<T[K], C[K], T, R> : never; | ||
}; | ||
} | ||
@@ -101,160 +94,164 @@ | ||
* applied to the result. | ||
* The Rimbu patch notation is as follows: | ||
* - if the target is a simple value or array, the patch can be the same type or a function returning the same type | ||
* - if the target is a tuple (array of fixed length), the patch be the same type or an object containing numeric keys with patches indicating the tuple index to patch | ||
* - if the target is an object, the patch can be the same type, or an array containing partial keys with their patches for the object | ||
* @typeparam T - the type of the value to patch | ||
* @typeparam TE - a utility type | ||
* @typeparam TT - a utility type | ||
* @param value - the input value to patch | ||
* @param patchItems - the `Patch` objects to apply to the input value | ||
* @param patchItem - the `Patch` value to apply to the input value | ||
* @example | ||
* ```ts | ||
* const input = { a: 1, b: { c: true, d: 'a' } } | ||
* patch(input, { a: 2 }) // => { a: 2, b: { c: true, d: 'a' } } | ||
* patch(input: ($) => ({ b: $({ c: v => !v }) }) ) | ||
* patch(input, [{ a: 2 }]) // => { a: 2, b: { c: true, d: 'a' } } | ||
* patch(input, [{ b: [{ c: (v) => !v }] }] ) | ||
* // => { a: 1, b: { c: false, d: 'a' } } | ||
* patch(input: ($) => ({ a: v => v + 1, b: $({ d: 'q' }) }) ) | ||
* patch(input: [{ a: (v) => v + 1, b: [{ d: 'q' }] }] ) | ||
* // => { a: 2, b: { c: true, d: 'q' } } | ||
* ``` | ||
*/ | ||
export function patch<T>(value: T & PlainObj<T>, ...patchItems: Patch<T>[]): T { | ||
const newValue = isPlainObj(value) ? { ...value } : value; | ||
const changedRef = { changed: false }; | ||
const result = processPatch(newValue, newValue, patchItems, changedRef); | ||
if (changedRef.changed) return result; | ||
return value; | ||
export function patch<T, TE extends T = T, TT = T>( | ||
value: T, | ||
patchItem: Patch<TE, TT> | ||
): T { | ||
return patchEntry(value, value, value, patchItem as Patch<T>); | ||
} | ||
/** | ||
* Interface providing a shared reference to a `changed` boolean. | ||
*/ | ||
interface ChangedRef { | ||
changed: boolean; | ||
} | ||
function processPatch<T, R>( | ||
function patchEntry<T, C, P, R>( | ||
value: T, | ||
parent: P, | ||
root: R, | ||
patchDataItems: Patch.Entry<T, R>[], | ||
changedRef: ChangedRef | ||
patchItem: Patch.Entry<T, C, P, R> | ||
): T { | ||
let i = -1; | ||
const len = patchDataItems.length; | ||
if (Object.is(value, patchItem)) { | ||
// patching a value with itself never changes the value | ||
return value; | ||
} | ||
while (++i < len) { | ||
const patchItem = patchDataItems[i]; | ||
if (patchItem instanceof Function) { | ||
const item = patchItem(patchNested); | ||
if (item instanceof NestedObj) { | ||
processPatch(value, root, item.patchDataItems, changedRef); | ||
} else { | ||
processPatchObj(value, root, item, changedRef); | ||
} | ||
} else { | ||
processPatchObj(value, root, patchItem, changedRef); | ||
} | ||
if (typeof value === 'function') { | ||
// function input, directly return patch | ||
return patchItem as T; | ||
} | ||
return value; | ||
} | ||
if (typeof patchItem === 'function') { | ||
// function patch always needs to be resolved first | ||
const item = patchItem(value, parent, root); | ||
function processPatchObj<T, R>( | ||
value: T, | ||
root: R, | ||
patchData: Patch.Obj<T, R>, | ||
changedRef: ChangedRef | ||
): T { | ||
if (undefined === value || null === value) { | ||
return value; | ||
return patchEntry(value, parent, root, item); | ||
} | ||
if (!isPlainObj(patchData)) { | ||
RimbuError.throwInvalidUsageError( | ||
'patch: received patch object should be a plain object' | ||
); | ||
if (isPlainObj(value)) { | ||
// value is plain object | ||
return patchPlainObj(value, root, patchItem as any); | ||
} | ||
if (!isPlainObj(value)) { | ||
RimbuError.throwInvalidUsageError( | ||
'patch: received source object should be a plain object' | ||
); | ||
if (Array.isArray(value)) { | ||
// value is tuple or array | ||
return patchArr(value, root, patchItem as any); | ||
} | ||
for (const key in patchData) { | ||
const target = value[key]; | ||
// value is primitive type or complex object | ||
// prevent prototype pollution | ||
if ( | ||
key === '__proto__' || | ||
(key === 'constructor' && target instanceof Function) | ||
) { | ||
RimbuError.throwInvalidUsageError( | ||
`patch: received patch object key '${key}' which is not allowed to prevent prototype pollution` | ||
); | ||
} | ||
return patchItem as T; | ||
} | ||
const update = patchData[key]; | ||
function patchPlainObj<T, C, R>( | ||
value: T, | ||
root: R, | ||
patchItem: T | Patch.Obj<T, C, R> | ||
): T { | ||
if (!Array.isArray(patchItem)) { | ||
// the patch is a complete replacement of the current value | ||
if (!(key in value) && update instanceof Function) { | ||
RimbuError.throwInvalidUsageError( | ||
`patch: received update function object key ${key} but the key was not present in the source object. Either explicitely set the value in the source to undefined or use a direct value.` | ||
); | ||
} | ||
return patchItem as T; | ||
} | ||
if (undefined === update) { | ||
RimbuError.throwInvalidUsageError( | ||
"patch: received 'undefined' as patch value. Due to type system issues we cannot prevent this through typing, but please use '() => undefined' or '() => yourVar' instead. This value will be ignored for safety." | ||
); | ||
} | ||
// patch is an array of partial updates | ||
let newValue: typeof target; | ||
// copy the input value | ||
const result = { ...value }; | ||
if (update instanceof Function) { | ||
newValue = processPatchObjItem( | ||
target, | ||
root, | ||
update(target, value, root), | ||
changedRef | ||
let anyChange = false; | ||
// loop over patches in array | ||
for (const entry of patchItem) { | ||
// update root if needed | ||
const currentRoot = (value as any) === root ? { ...result } : root; | ||
// loop over all the patch keys | ||
for (const key in entry as T) { | ||
// patch the value at the given key with the patch at that key | ||
const currentValue = result[key]; | ||
const newValue = patchEntry( | ||
currentValue, | ||
value, | ||
currentRoot, | ||
(entry as any)[key] | ||
); | ||
} else { | ||
newValue = processPatchObjItem( | ||
target, | ||
root, | ||
update as any, | ||
changedRef | ||
) as any; | ||
} | ||
if (!Object.is(newValue, target)) { | ||
value[key] = newValue; | ||
changedRef.changed = true; | ||
if (!Object.is(currentValue, newValue)) { | ||
// if value changed, set it in result and mark change | ||
anyChange = true; | ||
result[key] = newValue; | ||
} | ||
} | ||
} | ||
if (anyChange) { | ||
// something changed, return new value | ||
return result; | ||
} | ||
// nothing changed, return old value | ||
return value; | ||
} | ||
function processPatchObjItem<T, R>( | ||
function patchArr<T extends any[], C, R>( | ||
value: T, | ||
root: R, | ||
patchResult: Patch.ObjItem<T, R>, | ||
superChangedRef: ChangedRef | ||
patchItem: T | Patch.Tup<T, C, R> | ||
): T { | ||
if (patchResult instanceof NestedObj) { | ||
const newValue = isPlainObj(value) ? { ...value } : value; | ||
const changedRef = { changed: false }; | ||
if (Array.isArray(patchItem)) { | ||
// value is a normal array | ||
// patch is a complete replacement of current array | ||
const result = processPatch( | ||
newValue, | ||
return patchItem; | ||
} | ||
// value is a tuple | ||
// patch is an object containing numeric keys with function values | ||
// that update the tuple at the given indices | ||
// copy the tuple | ||
const result = [...value] as T; | ||
let anyChange = false; | ||
// loop over all index keys in object | ||
for (const index in patchItem) { | ||
const numIndex = index as any as number; | ||
// patch the tuple at the given index | ||
const currentValue = result[numIndex]; | ||
const newValue = patchEntry( | ||
currentValue, | ||
value, | ||
root, | ||
patchResult.patchDataItems, | ||
changedRef | ||
(patchItem as any)[index] | ||
); | ||
if (changedRef.changed) { | ||
superChangedRef.changed = true; | ||
return result; | ||
if (!Object.is(newValue, currentValue)) { | ||
// if value changed, set it in result and mark change | ||
anyChange = true; | ||
result[numIndex] = newValue; | ||
} | ||
} | ||
return value; | ||
if (anyChange) { | ||
// something changed, return new value | ||
return result; | ||
} | ||
return patchResult; | ||
// nothing changed, return old value | ||
return value; | ||
} |
468
src/path.ts
@@ -1,104 +0,430 @@ | ||
import type { Update } from '@rimbu/common'; | ||
import type { IsPlainObj, PlainObj } from '@rimbu/base'; | ||
import type { IsAnyFunc, IsArray, IsPlainObj } from '@rimbu/base'; | ||
import { Deep, Patch } from './internal'; | ||
import type { Tuple } from './tuple'; | ||
import { patch, patchNested } from './internal'; | ||
/** | ||
* A string representing a path into an (nested) object of type T. | ||
* @typeparam T - the object type to select in | ||
* @example | ||
* ```ts | ||
* const p: Path<{ a: { b: { c : 5 }}}> = 'a.b' | ||
* ``` | ||
*/ | ||
export type Path<T> = IsPlainObj<T> extends true | ||
? { [K in string & keyof T]: `${K}` | `${K}.${Path<T[K]>}` }[string & keyof T] | ||
: never; | ||
export namespace Path { | ||
/** | ||
* The result type when selecting from object type T a path with type P. | ||
* A string representing a path into an (nested) object of type T. | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @example | ||
* ```ts | ||
* let r!: Path.Result<{ a: { b: { c: number } } }, 'a.b'>; | ||
* // => type of r: { c: number } | ||
* const p: Path.Get<{ a: { b: { c : 5 } } }> = 'a.b' | ||
* ``` | ||
*/ | ||
export type Result<T, P extends Path<T> = Path<T>> = T extends Record< | ||
string, | ||
unknown | ||
> | ||
? P extends `${infer Head}.${infer Rest}` | ||
? Head extends keyof T | ||
? Path.Result<T[Head], Rest & Path<T[Head]>> | ||
: never | ||
: P extends `${infer K}` | ||
? T[K] | ||
: never | ||
: never; | ||
export type Get<T> = Path.Internal.Generic<T, false, false, true>; | ||
/** | ||
* Returns the value resulting from selecting the given `path` in the given `source` object. | ||
* A string representing a path into an (nested) object of type T. | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @param source - the object to select in | ||
* @param path - the path into the object | ||
* @example | ||
* ```ts | ||
* console.log(Path.get({ a: { b: { c: 5 } } }), 'a.b') | ||
* // => { c: 5 } | ||
* console.log(Path.get({ a: { b: { c: 5 } } }), 'a.b.c') | ||
* // => 5 | ||
* const p: Path.Set<{ a: { b: { c : 5 } } }> = 'a.b' | ||
* ``` | ||
*/ | ||
export function get<T, P extends Path<T> = Path<T>>( | ||
source: T & PlainObj<T>, | ||
path: P | ||
): Path.Result<T, P> { | ||
const items = path.split('.'); | ||
export type Set<T> = Path.Internal.Generic<T, true, false, true>; | ||
let result: any = source; | ||
export namespace Internal { | ||
/** | ||
* Determines the allowed paths into a value of type `T`. | ||
* @typeparam T - the source type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
* @typeparam First - if true this is the root call | ||
* @note type is mapped as template literal to prevent non-string types to leak through | ||
*/ | ||
export type Generic< | ||
T, | ||
Write extends boolean, | ||
Maybe extends boolean, | ||
First extends boolean = false | ||
> = `${IsAnyFunc<T> extends true | ||
? // functions can not be further decomposed | ||
'' | ||
: // empty string is always an option | ||
'' | Path.Internal.NonEmpty<T, Write, Maybe, First>}`; | ||
for (const item of items) { | ||
result = result[item]; | ||
} | ||
/** | ||
* Determines the allowed non-empty paths into a value of type `T`. | ||
* @typeparam T - the source type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
* @typeparam First - if true this is the root call | ||
*/ | ||
export type NonEmpty< | ||
T, | ||
Write extends boolean, | ||
Maybe extends boolean, | ||
First extends boolean | ||
> = Path.Internal.IsOptional<T> extends true | ||
? // the value T may be null or undefined, check whether further chaining is allowed | ||
Write extends false | ||
? // path is not used to write to, so optional chaining is allowed | ||
Path.Internal.Generic<Exclude<T, undefined | null>, Write, true> | ||
: // path can be written to, no optional chaining allowed | ||
never | ||
: // determine separator, and continue with non-optional value | ||
`${Path.Internal.Separator< | ||
First, | ||
Maybe, | ||
IsArray<T> | ||
>}${Path.Internal.NonOptional<T, Write, Maybe>}`; | ||
return result; | ||
/** | ||
* Determines the allowed paths into a non-optional value of type `T`. | ||
* @typeparam T - the source type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
* @typeparam First - if true this is the root call | ||
*/ | ||
export type NonOptional< | ||
T, | ||
Write extends boolean, | ||
Maybe extends boolean | ||
> = Tuple.IsTuple<T> extends true | ||
? // determine allowed paths for tuple | ||
Path.Internal.Tup<T, Write, Maybe> | ||
: T extends readonly any[] | ||
? // determine allowed paths for array | ||
Write extends false | ||
? // path is not writable so arrays are allowed | ||
Path.Internal.Arr<T> | ||
: // path is writable, no arrays allowed | ||
never | ||
: IsPlainObj<T> extends true | ||
? // determine allowed paths for object | ||
Path.Internal.Obj<T, Write, Maybe> | ||
: // no match | ||
never; | ||
/** | ||
* Determines the allowed paths for a tuple. Since tuples have fixed types, they do not | ||
* need to be optional, in contrast to arrays. | ||
* @typeparam T - the input tuple type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
*/ | ||
export type Tup<T, Write extends boolean, Maybe extends boolean> = { | ||
[K in Tuple.KeysOf<T>]: `[${K}]${Path.Internal.Generic< | ||
T[K], | ||
Write, | ||
Maybe | ||
>}`; | ||
}[Tuple.KeysOf<T>]; | ||
/** | ||
* Determines the allowed paths for an array. | ||
* @typeparam T - the input array type | ||
*/ | ||
export type Arr<T extends readonly any[]> = | ||
// first `[index]` and then the rest of the path, which cannot be Write (since optional) and must be Maybe | ||
`[${number}]${Path.Internal.Generic<T[number], false, true>}`; | ||
/** | ||
* Determines the allowed paths for an object. | ||
* @typeparam T - the input object type | ||
* @typeparam Write - if true the path should be writable (no optional chaining) | ||
* @typeparam Maybe - if true the value at the current path is optional | ||
*/ | ||
export type Obj<T, Write extends boolean, Maybe extends boolean> = { | ||
[K in keyof T]: `${K & string}${Path.Internal.Generic< | ||
T[K], | ||
Write, | ||
// If writable (not optional), Maybe is false. If value is optional, Maybe is true. Otherwise, forward current Maybe. | ||
Write extends true ? false : Path.Internal.IsOptional<T[K], true, Maybe> | ||
>}`; | ||
}[keyof T]; | ||
/** | ||
* Determines the allowed path part seperator based on the input types. | ||
* @typeparam First - if true, this is the first call | ||
* @typeparam Maybe - if true, the value is optional | ||
* @typeparam IsArray - if true, the value is an array | ||
*/ | ||
export type Separator< | ||
First extends boolean, | ||
Maybe extends boolean, | ||
IsArray extends boolean | ||
> = Maybe extends true | ||
? First extends true | ||
? // first optional value cannot have separator | ||
never | ||
: // non-first optional value must have separator | ||
'?.' | ||
: First extends true | ||
? // first non-optional value has empty separator | ||
'' | ||
: IsArray extends true | ||
? // array selectors do not have separator | ||
'' | ||
: // normal separator | ||
'.'; | ||
/** | ||
* Determines whether the given type `T` is optional, that is, whether it can be null or undefined. | ||
* @typeparam T - the input type | ||
* @typeparam True - the value to return if `T` is optional | ||
* @typeparam False - the value to return if `T` is mandatory | ||
*/ | ||
export type IsOptional<T, True = true, False = false> = undefined extends T | ||
? // is optional | ||
True | ||
: null extends T | ||
? // is optional | ||
True | ||
: // not optional | ||
False; | ||
/** | ||
* Returns type `T` if `Maybe` is false, `T | undefined` otherwise. | ||
* @typeparam T - the input type | ||
* @typeparam Maybe - if true, the return type value should be optional | ||
*/ | ||
export type MaybeValue<T, Maybe extends boolean> = Maybe extends true | ||
? T | undefined | ||
: T; | ||
/** | ||
* Utility type to only add non-empty string types to a string array. | ||
* @typeparma A - the input string array | ||
* @typeparam T - the string value to optionally add | ||
*/ | ||
export type AppendIfNotEmpty< | ||
A extends string[], | ||
T extends string | ||
> = T extends '' | ||
? // empty string, do not add | ||
A | ||
: // non-empty string, add to array | ||
[...A, T]; | ||
} | ||
/** | ||
* Sets the value at the given path in the source to the given value. | ||
* @param source - the object to update | ||
* @param path - the path in the object to update | ||
* @param value - the new value to set at the given position | ||
* The result type when selecting from object type T a path with type P. | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @example | ||
* ```ts | ||
* console.log(Path.update({ a: { b: { c: 5 } } }, 'a.b.c', v => v + 5) | ||
* // => { a: { b: { c: 6 } } } | ||
* let r!: Path.Result<{ a: { b: { c: number } } }, 'a.b'>; | ||
* // => type of r: { c: number } | ||
* ``` | ||
*/ | ||
export function update<T, P extends Path<T> = Path<T>>( | ||
source: T & PlainObj<T>, | ||
path: P, | ||
value: Update<Path.Result<T, P>> | ||
): T { | ||
const items = path.split('.'); | ||
const last = items.pop()!; | ||
export type Result<T, P extends string> = Path.Result.For< | ||
T, | ||
Path.Result.Tokenize<P>, | ||
false | ||
>; | ||
const root: Record<string, any> = {}; | ||
export namespace Result { | ||
/** | ||
* Determines the result type for an array of tokens representing subpaths in type `T`. | ||
* @typeparam T - the current source type | ||
* @typeparam Tokens - an array of elements indicating a path into the source type | ||
* @typeparam Maybe - if true indicates that the path may be undefined | ||
*/ | ||
export type For< | ||
T, | ||
Tokens, | ||
Maybe extends boolean = Path.Internal.IsOptional<T> | ||
> = Tokens extends [] | ||
? // no more token | ||
Path.Internal.MaybeValue<T, Maybe> | ||
: Path.Internal.IsOptional<T> extends true | ||
? // T can be null or undefined, so continue with Maybe set to true | ||
Path.Result.For<Exclude<T, undefined | null>, Tokens, Maybe> | ||
: Tokens extends ['?.', infer Key, ...infer Rest] | ||
? // optional chaining, process first part and set Maybe to true | ||
Path.Result.For<Path.Result.Part<T, Key, Maybe>, Rest, true> | ||
: Tokens extends ['.', infer Key, ...infer Rest] | ||
? // normal chaining, process first part and continue | ||
Path.Result.For<Path.Result.Part<T, Key, false>, Rest, Maybe> | ||
: Tokens extends [infer Key, ...infer Rest] | ||
? // process first part, and continue | ||
Path.Result.For<Path.Result.Part<T, Key, false>, Rest, Maybe> | ||
: never; | ||
let current = root; | ||
/** | ||
* Determines the result of getting the property/index `K` from type `T`, taking into | ||
* account that the value may be optional. | ||
* @typeparam T - the current source type | ||
* @typeparam K - the key to get from the source type | ||
* @typeparam Maybe - if true indicates that the path may be undefined | ||
*/ | ||
export type Part<T, K, Maybe extends boolean> = IsArray<T> extends true | ||
? // for arrays, Maybe needs to be set to true to force optional chaining | ||
// for tuples, Maybe should be false | ||
Path.Internal.MaybeValue< | ||
T[K & keyof T], | ||
Tuple.IsTuple<T> extends true ? Maybe : true | ||
> | ||
: // Return the type at the given key, and take `Maybe` into account | ||
Path.Internal.MaybeValue<T[K & keyof T], Maybe>; | ||
for (const item of items) { | ||
const next = {}; | ||
current[item] = patchNested(next); | ||
current = next; | ||
/** | ||
* Converts a path string into separate tokens in a string array. | ||
* @typeparam P - the literal string path type | ||
* @typeparam Token - the token currently being produced | ||
* @typeparam Res - the resulting literal string token array | ||
*/ | ||
export type Tokenize< | ||
P extends string, | ||
Token extends string = '', | ||
Res extends string[] = [] | ||
> = P extends '' | ||
? // no more input to process, return result | ||
Path.Internal.AppendIfNotEmpty<Res, Token> | ||
: P extends `[${infer Index}]${infer Rest}` | ||
? // input is an array selector, append index to tokens. Continue with new token | ||
Tokenize< | ||
Rest, | ||
'', | ||
[...Path.Internal.AppendIfNotEmpty<Res, Token>, Index] | ||
> | ||
: P extends `?.${infer Rest}` | ||
? // optional chaining, append to tokens. Continue with new token | ||
Tokenize< | ||
Rest, | ||
'', | ||
[...Path.Internal.AppendIfNotEmpty<Res, Token>, '?.'] | ||
> | ||
: P extends `.${infer Rest}` | ||
? // normal chaining, append to tokens. Continue with new token | ||
Tokenize<Rest, '', [...Path.Internal.AppendIfNotEmpty<Res, Token>, '.']> | ||
: P extends `${infer First}${infer Rest}` | ||
? // process next character | ||
Tokenize<Rest, `${Token}${First}`, Res> | ||
: never; | ||
} | ||
/** | ||
* Regular expression used to split a path string into tokens. | ||
*/ | ||
export const stringSplitRegex = /\?\.|\.|\[|\]/g; | ||
/** | ||
* The allowed values of a split path. | ||
*/ | ||
export type StringSplit = (string | number | undefined)[]; | ||
/** | ||
* Return the given `path` string split into an array of subpaths. | ||
* @param path - the input string path | ||
*/ | ||
export function stringSplit(path: string): Path.StringSplit { | ||
return path.split(Path.stringSplitRegex); | ||
} | ||
} | ||
/** | ||
* Returns the value resulting from selecting the given `path` in the given `source` object. | ||
* It supports optional chaining for nullable values or values that may be undefined, and also | ||
* for accessing objects inside an array. | ||
* There is currently no support for forcing non-null (the `!` operator). | ||
* @typeparam T - the object type to select in | ||
* @typeparam P - a Path in object type T | ||
* @param source - the object to select in | ||
* @param path - the path into the object | ||
* @example | ||
* ```ts | ||
* const value = { a: { b: { c: [{ d: 5 }, { d: 6 }] } } } | ||
* Deep.getAt(value, 'a.b'); | ||
* // => { c: 5 } | ||
* Deep.getAt(value, 'a.b.c'); | ||
* // => [{ d: 5 }, { d: 5 }] | ||
* Deep.getAt(value, 'a.b.c[1]'); | ||
* // => { d: 6 } | ||
* Deep.getAt(value, 'a.b.c[1]?.d'); | ||
* // => 6 | ||
* ``` | ||
*/ | ||
export function getAt<T, P extends Path.Get<T>>( | ||
source: T, | ||
path: P | ||
): Path.Result<T, P> { | ||
if (path === '') { | ||
// empty path always directly returns source value | ||
return source as any; | ||
} | ||
const items = Path.stringSplit(path); | ||
// start with `source` as result value | ||
let result = source as any; | ||
for (const item of items) { | ||
if (undefined === item || item === '' || item === '[') { | ||
// ignore irrelevant items | ||
continue; | ||
} | ||
current[last] = value; | ||
if (undefined === result || null === result) { | ||
// optional chaining assumed and no value available, skip rest of path and return undefined | ||
return undefined as any; | ||
} | ||
return patch<T>(source, root); | ||
// set current result to subpath value | ||
result = result[item]; | ||
} | ||
return result; | ||
} | ||
/** | ||
* Patches the value at the given path in the source to the given value. | ||
* Because the path to update must exist in the `source` object, optional | ||
* chaining and array indexing is not allowed. | ||
* @param source - the object to update | ||
* @param path - the path in the object to update | ||
* @param patchItem - the patch for the value at the given path | ||
* @example | ||
* ```ts | ||
* const value = { a: { b: { c: 5 } } }; | ||
* Deep.patchAt(value, 'a.b.c', v => v + 5); | ||
* // => { a: { b: { c: 6 } } } | ||
* ``` | ||
*/ | ||
export function patchAt<T, P extends Path.Set<T>, C = Path.Result<T, P>>( | ||
source: T, | ||
path: P, | ||
patchItem: Patch<Path.Result<T, P>, C> | ||
): T { | ||
if (path === '') { | ||
return Deep.patch(source, patchItem as any); | ||
} | ||
const items = Path.stringSplit(path); | ||
// creates a patch object based on the current path | ||
function createPatchPart(index: number, target: any): any { | ||
if (index === items.length) { | ||
// processed all items, return the input `patchItem` | ||
return patchItem; | ||
} | ||
const item = items[index]; | ||
if (undefined === item || item === '') { | ||
// empty items can be ignored | ||
return createPatchPart(index + 1, target); | ||
} | ||
if (item === '[') { | ||
// next item is array index, set arrayMode to true | ||
return createPatchPart(index + 1, target); | ||
} | ||
// create object with subPart as property key, and the restuls of processing next parts as value | ||
const result = { | ||
[item]: createPatchPart(index + 1, target[item]), | ||
}; | ||
if (Array.isArray(target)) { | ||
// target in source object is array/tuple, so the patch should be object | ||
return result; | ||
} | ||
// target in source is not an array, so it patch should be an array | ||
return [result]; | ||
} | ||
return Deep.patch(source, createPatchPart(0, source)); | ||
} |
@@ -15,31 +15,20 @@ import type { IsAny, IsPlainObj } from '@rimbu/base'; | ||
export type Protected<T> = IsAny<T> extends true | ||
? T | ||
? // to prevent infinite recursion, any will be any | ||
T | ||
: T extends readonly any[] & infer A | ||
? { readonly [K in keyof A]: Protected<A[K]> } | ||
? // convert all keys to readonly and all values to `Protected` | ||
{ readonly [K in keyof A]: Protected<A[K]> } | ||
: T extends Map<infer K, infer V> | ||
? Map<Protected<K>, Protected<V>> | ||
? // return keys and values as `Protected` and omit mutable methods | ||
Omit<Map<Protected<K>, Protected<V>>, 'clear' | 'delete' | 'set'> | ||
: T extends Set<infer E> | ||
? Set<Protected<E>> | ||
? // return values as `Protected` and omit mutable methods | ||
Omit<Set<Protected<E>>, 'add' | 'clear' | 'delete'> | ||
: T extends Promise<infer E> | ||
? Promise<Protected<E>> | ||
? // return promise value as `Protected` | ||
Promise<Protected<E>> | ||
: IsPlainObj<T> extends true | ||
? { readonly [K in keyof T]: Protected<T[K]> } | ||
: T; | ||
/** | ||
* Returns the same value wrapped in the `Protected` type. | ||
* @param value - the value to wrap | ||
* @note does not perform any runtime protection, it is only a utility to easily add the `Protected` | ||
* type to a value | ||
* @example | ||
* ```ts | ||
* const obj = Protected({ a: 1, b: { c: true, d: [1] } }) | ||
* obj.a = 2 // compiler error: a is readonly | ||
* obj.b.c = false // compiler error: c is readonly | ||
* obj.b.d.push(2) // compiler error: d is a readonly array | ||
* (obj as any).b.d.push(2) // will actually mutate the object | ||
* ``` | ||
*/ | ||
export function Protected<T>(value: T): Protected<T> { | ||
return value as any; | ||
} | ||
? // convert all keys to readonly and all values to `Protected` | ||
{ readonly [K in keyof T]: Protected<T[K]> } | ||
: // nothing to do, just return `T` | ||
T; |
@@ -20,3 +20,15 @@ import { Arr } from '@rimbu/base'; | ||
export type IsTuple<T> = T extends { length: infer L } | ||
? 0 extends L | ||
? false | ||
: true | ||
: false; | ||
/** | ||
* Returns the indices/keys that are in a tuple. | ||
* @typeparam T - the input tuple type | ||
*/ | ||
export type KeysOf<T> = { [K in keyof T]: K }[keyof T & number]; | ||
/** | ||
* Convenience method to type Tuple types | ||
@@ -23,0 +35,0 @@ * @param values - the values of the tuple |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
203441
57
4351
1
+ Added@rimbu/base@0.10.1(transitive)
+ Added@rimbu/common@0.11.0(transitive)
- Removed@rimbu/base@0.9.5(transitive)
Updated@rimbu/base@^0.10.0