Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Jan 16, 2022
1 parent a88b2fe commit 18043a9
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 125 deletions.
63 changes: 63 additions & 0 deletions src/config/normalize/prop_path/entries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import isPlainObj from 'is-plain-obj'

import { parseQuery } from './parse.js'

// List all values (and their associated path) matching a specific query for
// on specific target value.
export const listEntries = function (target, query) {
const tokens = parseQuery(query)
return tokens.reduce(listTokenEntries, [{ value: target, path: [] }])
}

const listTokenEntries = function (entries, token) {
return entries.flatMap((entry) => getTokenEntries(entry, token))
}

const getTokenEntries = function (
{ value, path },
{ key, isArray, isAny, isStrict },
) {
if (isArray) {
const missing = !Array.isArray(value)

if (missing) {
if (isStrict) {
if (isAny || key === 0) {
return [{ value, path: [...path, { key: 0, missing }] }]
}

return [{ value: undefined, path: [...path, { key, missing }] }]
}

return []
}

if (isAny) {
return value.map((childValue, index) => ({
value: childValue,
path: [...path, { key: index, missing }],
}))
}

return [{ value: value[key], path: [...path, { key, missing }] }]
}

const missing = !isPlainObj(value)

if (missing) {
if (isStrict) {
return [{ value: undefined, path: [...path, { key, missing }] }]
}

return []
}

if (isAny) {
return Object.entries(value).map(([childKey, childValue]) => ({
value: childValue,
path: [...path, { key: childKey, missing }],
}))
}

return [{ value: value[key], path: [...path, { key, missing }] }]
}
47 changes: 25 additions & 22 deletions src/config/normalize/prop_path/get.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,44 @@
import { getPropResults } from './results.js'
import { listEntries } from './entries.js'

export const getEntries = function (object, propPathStr) {
const propResults = getPropResults(object, propPathStr)
return propResults.map(getEntry)
// Retrieve all entries (values + paths) matching a query string in `target`
export const getEntries = function (target, query) {
const entries = listEntries(target, query)
return entries.map(normalizeEntry)
}

const getEntry = function ({ value, path }) {
const key = getKey({ path })
return { key, value }
const normalizeEntry = function ({ value, path }) {
const pathStr = serializePath({ path })
return { path: pathStr, value }
}

export const getValues = function (object, propPathStr) {
const propResults = getPropResults(object, propPathStr)
return propResults.map(getValue)
// Same but only retrieving the values
export const getValues = function (target, query) {
const entris = listEntries(target, query)
return entris.map(getEntryValue)
}

const getValue = function ({ value }) {
const getEntryValue = function ({ value }) {
return value
}

export const getKeys = function (object, propPathStr) {
const propResults = getPropResults(object, propPathStr)
return propResults.map(getKey)
// Same but only retrieving the paths
export const getPaths = function (target, query) {
const entries = listEntries(target, query)
return entries.map(serializePath)
}

const getKey = function ({ path }) {
return path.map(getPathName).reduce(serializePathName, '')
const serializePath = function ({ path }) {
return path.map(getPathKey).reduce(appendKey, '')
}

const getPathName = function ({ name }) {
return name
const getPathKey = function ({ key }) {
return key
}

const serializePathName = function (names, name) {
if (typeof name !== 'string') {
return `${names}[${name}]`
const appendKey = function (pathStr, key) {
if (typeof key !== 'string') {
return `${pathStr}[${key}]`
}

return names === '' ? `${names}${name}` : `${names}.${name}`
return pathStr === '' ? `${pathStr}${key}` : `${pathStr}.${key}`
}
59 changes: 29 additions & 30 deletions src/config/normalize/prop_path/parse.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
// Parse a configuration property path string into an array of tokens.
// Parse a query string into an array of tokens.
// Dots are used for object properties, e.g. `one.two`
// Brackets are used for array elements, e.g. `one[5]`
// Dots and brackets can be used deeply, e.g. `one.two[5]`
// Wildcards are used with both objects and arrays to recurse over their
// children, e.g. `one.*` or `one[*]`.
// Unless question marks are appended to dots and brackets, the parent objects
// or arrays will be validated as such and created if undefined.
// Can start with an optional dot.
// or arrays will be created if undefined.
// TODO: allow special characters (like dots), if escaped with backslash
// TODO: do not recurse over `__proto__`, `prototype` or `constructor`
export const parsePropPath = function (propPathStr) {
const normPropPathStr = prependDot(propPathStr)
const results = [...normPropPathStr.matchAll(PROP_NAME_REGEXP)]
validatePropName(results, propPathStr, normPropPathStr)
const propPath = results.map(normalizeProp)
return propPath
export const parseQuery = function (query) {
const normalizedQuery = prependDot(query)
const matchResults = [...normalizedQuery.matchAll(QUERY_REGEXP)]
validateQuery(matchResults, query, normalizedQuery)
const tokens = matchResults.map(getToken)
return tokens
}

const prependDot = function (propPathStr) {
const [firstChar] = propPathStr
return firstChar !== '.' && firstChar !== '['
? `.${propPathStr}`
: propPathStr
// Queries can start with an optional dot.
const prependDot = function (query) {
const [firstChar] = query
return firstChar !== '.' && firstChar !== '[' ? `.${query}` : query
}

const PROP_NAME_REGEXP =
/(?<loose>\?)?((\.(?<propName>([^.?[\]*\d][^.?[\]*]*|(?<propNameAny>\*))))|(\[(?<propIndex>([\d]+|(?<propIndexAny>\*)))\]))/guy
const QUERY_REGEXP =
/(?<loose>\?)?((\.(?<name>([^.?[\]*\d][^.?[\]*]*|(?<nameAny>\*))))|(\[(?<index>([\d]+|(?<indexAny>\*)))\]))/guy

// Validate against syntax errors in the query
// TODO: add more error messages for common mistakes
const validatePropName = function (results, propPathStr, normPropPathStr) {
const matchedPath = results.map(getMatch).join('')
const validateQuery = function (matchResults, query, normalizedQuery) {
const matchedQuery = matchResults.map(getMatch).join('')

if (matchedPath !== normPropPathStr) {
if (matchedQuery !== normalizedQuery) {
throw new Error(
`Syntax error in path "${propPathStr}" (starting at index ${matchedPath.length})`,
`Syntax error in path "${query}" (starting at index ${matchedQuery.length})`,
)
}
}
Expand All @@ -42,20 +41,20 @@ const getMatch = function ([match]) {
return match
}

const normalizeProp = function ({
groups: { propName, propNameAny, propIndex, propIndexAny, loose },
const getToken = function ({
groups: { name, nameAny, index, indexAny, loose },
}) {
const name = getName(propName, propIndex)
const isArray = propIndex !== undefined
const isAny = propNameAny !== undefined || propIndexAny !== undefined
const key = getKey(name, index)
const isArray = index !== undefined
const isAny = nameAny !== undefined || indexAny !== undefined
const isStrict = loose === undefined
return { name, isArray, isAny, isStrict }
return { key, isArray, isAny, isStrict }
}

const getName = function (propName, propIndex) {
if (propName !== undefined) {
return propName
const getKey = function (name, index) {
if (name !== undefined) {
return name
}

return propIndex === '*' ? propIndex : Number(propIndex)
return index === '*' ? index : Number(index)
}
61 changes: 0 additions & 61 deletions src/config/normalize/prop_path/results.js

This file was deleted.

25 changes: 13 additions & 12 deletions src/config/normalize/prop_path/set.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import { setArray } from '../../../utils/set.js'

import { getPropResults } from './results.js'
import { listEntries } from './entries.js'

export const set = function (object, propPathStr, setValue) {
const propResults = getPropResults(object, propPathStr)
return propResults.reduce(
(objectA, { path }) => setResult(objectA, path, setValue),
object,
// Set a value to one or multiple properties in `target` using a query string
export const set = function (target, query, setValue) {
const entries = listEntries(target, query)
return entries.reduce(
(targetA, { path }) => setResult(targetA, path, setValue),
target,
)
}

const setResult = function (value, [{ name, missing }, ...path], setValue) {
if (typeof name === 'string') {
const setResult = function (value, [{ key, missing }, ...path], setValue) {
if (typeof key === 'string') {
const setValueA =
path.length === 0
? setValue
: setResult(missing ? {} : value[name], path, setValue)
return { ...value, [name]: setValueA }
: setResult(missing ? {} : value[key], path, setValue)
return { ...value, [key]: setValueA }
}

const valueA = missing ? [value] : value
const setValueB =
path.length === 0 ? setValue : setResult(valueA[name], path, setValue)
return setArray(valueA, name, setValueB)
path.length === 0 ? setValue : setResult(valueA[key], path, setValue)
return setArray(valueA, key, setValueB)
}

0 comments on commit 18043a9

Please sign in to comment.