Skip to content

Commit

Permalink
Add more logic to names
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Jan 16, 2022
1 parent c27f61b commit 40a26b6
Showing 1 changed file with 100 additions and 21 deletions.
121 changes: 100 additions & 21 deletions src/config/normalize/name.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import isPlainObj from 'is-plain-obj'

import { setArray } from '../../utils/set.js'

// Parse a configuration property path 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]`
Expand All @@ -8,27 +10,36 @@ import isPlainObj from 'is-plain-obj'
// 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.
export const parsePropPath = function (propPathStr) {
const results = [...`.${propPathStr}`.matchAll(PROP_NAME_REGEXP)]
validatePropName(results, propPathStr)
// Can start with an optional dot.
// TODO: allow special characters (like dots), if escaped with backslash
// TODO: do not recurse over `__proto__`, `prototype` or `constructor`
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
}

const prependDot = function (propPathStr) {
const [firstChar] = propPathStr
return firstChar !== '.' && firstChar !== '['
? `.${propPathStr}`
: propPathStr
}

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

// TODO: add more error messages for common mistakes
const validatePropName = function (results, propPathStr) {
const matchedPath = results.map(getMatch).join('').slice(1)
const validatePropName = function (results, propPathStr, normPropPathStr) {
const matchedPath = results.map(getMatch).join('')

if (matchedPath === propPathStr) {
return
if (matchedPath !== normPropPathStr) {
throw new Error(
`Syntax error in path "${propPathStr}" (starting at index ${matchedPath.length})`,
)
}

throw new Error(
`Syntax error in path "${propPathStr}" (starting at index ${matchedPath.length})`,
)
}

const getMatch = function ([match]) {
Expand All @@ -53,7 +64,71 @@ const getName = function (propName, propIndex) {
return propIndex === '*' ? propIndex : Number(propIndex)
}

export const getByPropPath = function (value, propPath) {
export const getEntries = function (object, propPathStr) {
const propResults = getPropResults(object, propPathStr)
return propResults.map(getEntry)
}

const getEntry = function ({ value, path }) {
const key = getKey({ path })
return { key, value }
}

export const getValues = function (object, propPathStr) {
const propResults = getPropResults(object, propPathStr)
return propResults.map(getValue)
}

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

export const getKeys = function (object, propPathStr) {
const propResults = getPropResults(object, propPathStr)
return propResults.map(getKey)
}

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

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

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

return names === '' ? `${names}${name}` : `${names}.${name}`
}

export const set = function (object, propPathStr, setValue) {
const propResults = getPropResults(object, propPathStr)
return propResults.reduce(
(objectA, { path }) => setResult(objectA, path, setValue),
object,
)
}

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

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

const getPropResults = function (value, propPathStr) {
const propPath = parsePropPath(propPathStr)
return propPath.reduce(getValuesByProp, [{ value, path: [] }])
}

Expand All @@ -66,13 +141,15 @@ const getValueByProp = function (
{ name, isArray, isAny, isStrict },
) {
if (isArray) {
if (!Array.isArray(value)) {
const missing = !Array.isArray(value)

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

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

return []
Expand All @@ -81,16 +158,18 @@ const getValueByProp = function (
if (isAny) {
return value.map((childValue, index) => ({
value: childValue,
path: [...path, index],
path: [...path, { name: index, missing }],
}))
}

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

if (!isPlainObj(value)) {
const missing = !isPlainObj(value)

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

return []
Expand All @@ -99,9 +178,9 @@ const getValueByProp = function (
if (isAny) {
return Object.entries(value).map(([childName, childValue]) => ({
value: childValue,
path: [...path, childName],
path: [...path, { name: childName, missing }],
}))
}

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

0 comments on commit 40a26b6

Please sign in to comment.