lodash-walk-object
Version:
Walk all properties deep in object with lodash help
446 lines (441 loc) • 15.9 kB
JavaScript
import { _ } from 'tnp-core/browser';
class Helpers {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
static get Walk() {
const self = this;
return {
Object(json, iterator, options = {}) {
const internalOptions = self.createInternalOptions(options, _.isFunction(iterator));
const result = self._walk(json, json, iterator, '', internalOptions);
return {
circs: result.circural,
};
},
ObjectBy(property, inContext, iterator, options = {}) {
const contextRecord = inContext;
if (_.isFunction(iterator)) {
iterator(inContext, '', self._changeValue(inContext, property, true));
}
const json = contextRecord[property];
if (!self.isObjectLike(json)) {
return {
circs: undefined,
};
}
return self.Walk.Object(json, iterator, options);
},
};
}
static createInternalOptions(options, hasIterator) {
const internalOptions = {
...options,
walkGetters: options.walkGetters ?? true,
checkCircural: options.checkCircural ?? false,
breadthWalk: options.breadthWalk ?? false,
considerSharedObjects: options.considerSharedObjects ?? false,
include: options.include ?? [],
exclude: options.exclude ?? [],
isGetter: false,
isCircural: false,
hasIterator,
_valueChanged: false,
_skip: false,
_exit: false,
};
internalOptions.skipObject = () => {
internalOptions._skip = true;
};
internalOptions.exit = () => {
internalOptions._exit = true;
};
if (internalOptions.checkCircural) {
internalOptions.stack = [];
internalOptions.circural = [];
}
return internalOptions;
}
static _walk(json, currentValue, iterator, lodashPath, options) {
if (options.breadthWalk) {
return this.walkBreadthFirst(json, iterator, lodashPath, options);
}
this.walkDepthFirst(json, currentValue, iterator, lodashPath, options, false);
return options;
}
static walkDepthFirst(json, currentValue, iterator, lodashPath, options, isGetter) {
if (options._exit) {
return;
}
if (this.shouldSkipPath(options.include, options.exclude, lodashPath)) {
return;
}
const pushedToStack = this.prepareCurrentValue(currentValue, lodashPath, options, isGetter);
try {
if (options._exit) {
return;
}
if (options.hasIterator && lodashPath !== '') {
iterator(currentValue, lodashPath, this._changeValue(json, lodashPath, false, options), options);
}
if (options._exit) {
return;
}
if (options._valueChanged) {
currentValue = _.get(json, lodashPath);
options._valueChanged = false;
}
if (options.isCircural) {
return;
}
if (options._skip) {
options._skip = false;
return;
}
const children = this.findChildren(currentValue, lodashPath, options.walkGetters ?? true);
for (const child of children) {
if (options._exit) {
return;
}
this.walkDepthFirst(json, child.value, iterator, child.path, options, child.isGetter);
}
}
finally {
if (pushedToStack && options.considerSharedObjects) {
options.stack?.pop();
}
}
}
static walkBreadthFirst(json, iterator, lodashPath, options) {
const queue = [
{
v: json,
p: lodashPath,
isGetter: false,
ancestors: [],
},
];
while (queue.length > 0) {
if (options._exit) {
break;
}
const current = queue.shift();
if (!current) {
break;
}
if (this.shouldSkipPath(options.include, options.exclude, current.p)) {
continue;
}
options.isGetter = current.isGetter ?? false;
options.isCircural = false;
let childAncestors = current.ancestors ?? [];
if (options.checkCircural && this.isObjectLike(current.v)) {
if (options.considerSharedObjects) {
const existingAncestor = childAncestors.find(entry => entry.target === current.v);
if (existingAncestor) {
options.circural ??= [];
options.circural.push({
pathToObj: current.p,
circuralTargetPath: existingAncestor.path,
});
options.isCircural = true;
}
else {
childAncestors = [
...childAncestors,
{
target: current.v,
path: current.p,
},
];
}
}
else {
options.stack ??= [];
options.circural ??= [];
const existing = options.stack.find(entry => entry.target === current.v);
if (existing) {
options.circural.push({
pathToObj: current.p,
circuralTargetPath: existing.path,
});
options.isCircural = true;
}
else {
options.stack.push({
target: current.v,
path: current.p,
});
}
}
}
if (options.hasIterator && current.p !== '') {
iterator(current.v, current.p, this._changeValue(json, current.p, false, options), options);
}
if (options._exit) {
break;
}
if (options._valueChanged) {
current.v = _.get(json, current.p);
options._valueChanged = false;
}
if (options.isCircural) {
continue;
}
if (options._skip) {
options._skip = false;
continue;
}
const children = this.findChildren(current.v, current.p, options.walkGetters ?? true);
for (const child of children) {
queue.push({
v: child.value,
p: child.path,
parent: current,
isGetter: child.isGetter,
ancestors: childAncestors,
});
}
}
return options;
}
static prepareCurrentValue(value, lodashPath, options, isGetter) {
options.isGetter = isGetter;
options.isCircural = false;
if (!options.checkCircural || !this.isObjectLike(value)) {
return false;
}
const stack = options.stack ?? [];
const circular = options.circural ?? [];
options.stack = stack;
options.circural = circular;
const existing = stack.find(entry => entry.target === value);
if (existing) {
circular.push({
pathToObj: lodashPath,
circuralTargetPath: existing.path,
});
options.isCircural = true;
return false;
}
stack.push({
target: value,
path: lodashPath,
});
return true;
}
static findChildren(value, parentPath, walkGetters) {
if (Array.isArray(value)) {
return value.map((child, index) => ({
value: child,
path: `${parentPath}[${index}]`,
isGetter: false,
}));
}
if (!this.isObjectLike(value)) {
return [];
}
const entries = this.getPropertyEntries(value, walkGetters);
return entries.map(entry => ({
value: entry.value,
path: this.appendPath(parentPath, entry.key),
isGetter: entry.isGetter,
}));
}
static getPropertyEntries(object, walkGetters) {
const result = [];
const handledKeys = new Set();
for (const key of Object.keys(object)) {
const readResult = this.readProperty(object, key);
if (!readResult.success) {
continue;
}
handledKeys.add(key);
result.push({
key,
value: readResult.value,
isGetter: false,
});
}
if (!walkGetters) {
return result;
}
for (const key of Object.getOwnPropertyNames(object)) {
if (handledKeys.has(key)) {
continue;
}
const readResult = this.readProperty(object, key);
if (!readResult.success) {
continue;
}
handledKeys.add(key);
result.push({
key,
value: readResult.value,
isGetter: true,
});
}
let prototype = Object.getPrototypeOf(object);
while (prototype && prototype !== Object.prototype) {
const descriptors = Object.getOwnPropertyDescriptors(prototype);
for (const [key, descriptor] of Object.entries(descriptors)) {
if (key === 'constructor' ||
handledKeys.has(key) ||
typeof descriptor.get !== 'function') {
continue;
}
const readResult = this.readProperty(object, key);
if (!readResult.success) {
continue;
}
handledKeys.add(key);
result.push({
key,
value: readResult.value,
isGetter: true,
});
}
prototype = Object.getPrototypeOf(prototype);
}
return result;
}
static readProperty(object, key) {
try {
return {
success: true,
value: Reflect.get(object, key),
};
}
catch {
return {
success: false,
};
}
}
static appendPath(parentPath, property) {
const propertyPath = this.propertyToPath(property);
if (parentPath === '') {
return propertyPath;
}
if (propertyPath.startsWith('[')) {
return `${parentPath}${propertyPath}`;
}
return `${parentPath}.${propertyPath}`;
}
static propertyToPath(property) {
/**
* Preserve the old path format for keys that lodash can safely
* interpret in dot notation.
*
* Only keys containing dots or brackets need quoted bracket syntax,
* because those characters change the lodash path meaning.
*/
if (property !== '' &&
!property.includes('.') &&
!property.includes('[') &&
!property.includes(']')) {
return property;
}
return `[${JSON.stringify(property)}]`;
}
static shouldSkipPath(include = [], exclude = [], lodashPath) {
const normalizedPath = this.normalizeRootArrayPath(lodashPath);
if (normalizedPath === '') {
return false;
}
const excluded = exclude.some(excludedPath => this.isSamePathOrDescendant(normalizedPath, excludedPath));
if (excluded) {
return true;
}
if (include.length === 0) {
return false;
}
const includedOrRequiredParent = include.some(includedPath => {
return (this.isSamePathOrDescendant(normalizedPath, includedPath) ||
this.isAncestorPath(normalizedPath, includedPath));
});
return !includedOrRequiredParent;
}
static normalizeRootArrayPath(path) {
return path.replace(/^\[(?:'|")?\d+(?:'|")?\]\.?/, '').trim();
}
static isSamePathOrDescendant(path, parentPath) {
return (path === parentPath ||
path.startsWith(`${parentPath}.`) ||
path.startsWith(`${parentPath}[`));
}
static isAncestorPath(possibleAncestor, path) {
return (path.startsWith(`${possibleAncestor}.`) ||
path.startsWith(`${possibleAncestor}[`));
}
static _changeValue(json, lodashPath, simpleChange = false, options) {
const { contextPath, property } = this._prepareParams(lodashPath);
return (newValue) => {
if (contextPath === '') {
simpleChange = true;
}
if (simpleChange) {
Reflect.set(json, property, newValue);
}
else {
const context = _.get(json, contextPath);
if (this.isObjectLike(context)) {
Reflect.set(context, property, newValue);
}
}
if (options) {
options._valueChanged = true;
}
};
}
static _prepareParams(lodashPath) {
const contextPath = this._Helpers.Path.getContextPath(lodashPath);
const rawProperty = this._Helpers.Path.getPropertyPath(lodashPath, contextPath);
const property = rawProperty.trim() !== '' && !Number.isNaN(Number(rawProperty))
? Number(rawProperty)
: rawProperty;
return {
contextPath,
property,
};
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
static get _Helpers() {
return {
get Path() {
return {
getPropertyPath(lodashPath, contextPath) {
return lodashPath
.replace(contextPath, '')
.replace(/^\./, '')
.replace(/^\[/, '')
.replace(/\]$/, '')
.replace(/^["']|["']$/g, '');
},
getContextPath(path) {
let result;
if (path.endsWith(']')) {
result = path.replace(/\[(?:"|')?.+?(?:"|')?\]$/, '');
}
else {
result = path.replace(/\.([a-zA-Z0-9_$@\-/:]+)$/, '');
}
return result === path ? '' : result;
},
};
},
};
}
static isObjectLike(value) {
return (value !== null &&
(typeof value === 'object' || typeof value === 'function'));
}
}
const walk = {
Object: Helpers.Walk.Object,
ObjectBy: Helpers.Walk.ObjectBy,
};
// export * from './start-cli'; // @backend
/**
* Generated bundle index. Do not edit.
*/
export { Helpers, walk };
//# sourceMappingURL=lodash-walk-object-browser.mjs.map