Skip to content

Commit

Permalink
Start optimized path
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Apr 3, 2022
1 parent b0cc736 commit 5522d27
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 2 deletions.
52 changes: 50 additions & 2 deletions src/iterate/path.js
Original file line number Diff line number Diff line change
@@ -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)
}
45 changes: 45 additions & 0 deletions src/iterate/path_children.js
Original file line number Diff line number Diff line change
@@ -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)
}
56 changes: 56 additions & 0 deletions src/iterate/path_expand.js
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 30 additions & 0 deletions src/iterate/path_missing.js
Original file line number Diff line number Diff line change
@@ -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,
},
}

0 comments on commit 5522d27

Please # to comment.