diff --git a/src/iterate/path.js b/src/iterate/path.js index 4e4c9ac..d8bdb89 100644 --- a/src/iterate/path.js +++ b/src/iterate/path.js @@ -1,6 +1,54 @@ -import { iterateQuery } from './query.js' +import { iterateChildEntries } from './path_children.js' // `iterate()` logic when the query is path export const iteratePath = function* (target, pathArray, opts) { - yield* iterateQuery(target, [pathArray], opts) + const entries = getRootEntries(target, pathArray) + yield* iterateLevel(entries, 0, opts) +} + +const getRootEntries = function (target, pathArray) { + return [{ queryArray: pathArray, value: target, path: [], missing: false }] +} + +// The `roots` option can be used to only include the highest ancestors. +// The `leaves` option can be used to only include the lowest descendants. +// Neither option includes the values in-between. +const iterateLevel = function* (entries, index, opts) { + const parentEntry = getParentEntry(entries, index) + + if (shouldYieldParentFirst(parentEntry, opts)) { + yield normalizeEntry(parentEntry, opts) + } + + const hasChildren = yield* iterateChildEntries({ + entries, + parentEntry, + index, + opts, + iterateLevel, + }) + + if (shouldYieldParentLast(parentEntry, hasChildren, opts)) { + yield normalizeEntry(parentEntry, opts) + } +} + +const getParentEntry = function (entries, index) { + return entries.find(({ queryArray }) => queryArray.length === index) +} + +const normalizeEntry = function ({ value, path, missing }, { entries }) { + return entries ? { value, path, missing } : value +} + +const shouldYieldParentFirst = function (parentEntry, { childFirst }) { + return parentEntry !== undefined && !childFirst +} + +const shouldYieldParentLast = function ( + parentEntry, + hasChildren, + { childFirst, leaves }, +) { + return parentEntry !== undefined && childFirst && !(leaves && hasChildren) } diff --git a/src/iterate/path_children.js b/src/iterate/path_children.js new file mode 100644 index 0000000..d2e6798 --- /dev/null +++ b/src/iterate/path_children.js @@ -0,0 +1,45 @@ +import { expandTokens } from './path_expand.js' + +// Iterate over child entries +export const iterateChildEntries = function* ({ + entries, + parentEntry, + index, + opts, + iterateLevel, +}) { + if (!shouldIterateChildren(entries, parentEntry, opts)) { + return false + } + + // eslint-disable-next-line fp/no-let + let hasChildren = false + + // eslint-disable-next-line fp/no-loops + for (const childEntry of iterateChildren({ + entries, + index, + opts, + iterateLevel, + })) { + // eslint-disable-next-line fp/no-mutation + hasChildren = true + yield childEntry + } + + return hasChildren +} + +const shouldIterateChildren = function (entries, parentEntry, { roots }) { + return parentEntry === undefined || (entries.length !== 1 && !roots) +} + +const iterateChildren = function* ({ entries, index, opts, iterateLevel }) { + const childEntries = expandTokens(entries, index, opts) + + if (childEntries.length === 0) { + return + } + + yield* iterateLevel(childEntries, index + 1, opts) +} diff --git a/src/iterate/path_expand.js b/src/iterate/path_expand.js new file mode 100644 index 0000000..6f3eef1 --- /dev/null +++ b/src/iterate/path_expand.js @@ -0,0 +1,56 @@ +import { handleMissingValue } from './path_missing.js' + +// Expand special tokens like *, **, regexps, slices into property names or +// indices for a given value +export const expandTokens = function (entries, index, opts) { + return entries + .filter(({ queryArray }) => queryArray.length !== index) + .flatMap((entry) => expandToken(entry, index, opts)) +} + +// Use the token to list entries against a target value. +const expandToken = function ({ queryArray, value, path }, index, opts) { + const token = queryArray[index] + const missingReturn = handleMissingValue(value, token, opts.classes) + const childEntriesA = iterateToken(token, missingReturn, opts) + return childEntriesA + .filter(isAllowedProp) + .map(({ value: childValue, prop, missing: missingEntry }) => ({ + queryArray, + value: childValue, + path: [...path, prop], + missing: missingReturn.missing || missingEntry, + })) +} + +const isAllowedProp = function ({ prop }) { + return !FORBIDDEN_PROPS.has(prop) +} + +// Forbidden to avoid prototype pollution attacks +const FORBIDDEN_PROPS = new Set(['__proto__', 'prototype', 'constructor']) + +const iterateToken = function ( + token, + { missing: missingParent, value }, + { inherited, missing: includeMissing }, +) { + if (includeMissing) { + return iterate(value, token, inherited) + } + + if (missingParent) { + return [] + } + + const childEntries = iterate(value, token, inherited) + return childEntries.filter(isNotMissing) +} + +const iterate = function (value, token) { + return [{ value: value[token], prop: token, missing: !(token in value) }] +} + +const isNotMissing = function ({ missing }) { + return !missing +} diff --git a/src/iterate/path_missing.js b/src/iterate/path_missing.js new file mode 100644 index 0000000..fe53fa8 --- /dev/null +++ b/src/iterate/path_missing.js @@ -0,0 +1,30 @@ +import { getTokenType } from '../tokens/main.js' + +import { isWeakObject } from './object.js' + +export const handleMissingValue = function (value, token, classes) { + const tokenType = getTokenType(token) + const { isPresent, getDefaultValue } = MISSING_HANDLERS[tokenType.valueType] + const missing = !isPresent(value, classes) + const valueA = missing ? getDefaultValue() : value + return { missing, value: valueA } +} + +const getDefaultObject = function () { + return {} +} + +const getDefaultArray = function () { + return [] +} + +const MISSING_HANDLERS = { + array: { + isPresent: Array.isArray, + getDefaultValue: getDefaultArray, + }, + weakObject: { + isPresent: isWeakObject, + getDefaultValue: getDefaultObject, + }, +}