Skip to content

Commit

Permalink
Improve parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Feb 27, 2022
1 parent 7a02c6a commit 0bf4ac7
Showing 1 changed file with 52 additions and 22 deletions.
74 changes: 52 additions & 22 deletions src/config/normalize/lib/star_dot_path/parse.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import isPlainObj from 'is-plain-obj'

// Parse a query string into an array of tokens.
// This is similar to JSON paths but:
// - simpler, with fewer features
Expand All @@ -12,7 +14,7 @@
// - Empty keys are supported, e.g. `one.` for `{ one: { "": value } }`
// or `one..two` for `{ one: { "": { two: value } } }`
// - An empty string matches the root value
// - Backslashes can escape dots
// - Backslashes can escape special characters: . * \
// We allow passing an array of tokens instead of a string with the above syntax
// - This is sometimes more convenient
// - Also, this allows property names to include special characters (dots,
Expand All @@ -35,48 +37,67 @@ export const parse = function (query) {
}

const tokens = []
let token = ''
let token = []
let tokenPart = ''
let index = query[0] === SEPARATOR ? 1 : 0

for (; index < query.length; index += 1) {
for (; index <= query.length; index += 1) {
const character = query[index]

if (character === ESCAPE) {
index += 1
const escapedCharacter = query[index]
validateEscape(escapedCharacter, query, character, index)
token += escapedCharacter
tokenPart += escapedCharacter
continue
}

if (character === SEPARATOR || index === query.length) {
if (tokenPart !== '' || token.length === 0) {
const tokenPartA =
token.length === 0 && POSITIVE_INTEGER_REGEXP.test(tokenPart)
? Number(tokenPart)
: tokenPart
token.push(tokenPartA)
tokenPart = ''
}

tokens.push(token)
token = []
continue
}

if (character === SEPARATOR) {
tokens.push(parseIndex(token))
token = ''
if (character === ANY) {
if (tokenPart !== '') {
token.push(tokenPart)
tokenPart = ''
}

token.push({ type: ANY_TYPE })
continue
}

token += character
tokenPart += character
}

tokens.push(parseIndex(token))
return tokens
}
/* eslint-enable complexity, max-depth, max-statements, fp/no-loops,
fp/no-mutation, fp/no-let, no-continue, fp/no-mutating-methods */

// eslint-disable-next-line max-params
const validateEscape = function (escapedCharacter, query, character, index) {
if (escapedCharacter !== ESCAPE && escapedCharacter !== SEPARATOR) {
if (
escapedCharacter !== ESCAPE &&
escapedCharacter !== SEPARATOR &&
escapedCharacter !== ANY
) {
throw new Error(
`Invalid query "${query}": character ${character} at index ${index} must be followed by ${SEPARATOR} or ${ESCAPE}`,
`Invalid query "${query}": character ${character} at index ${index} must be followed by ${SEPARATOR}, ${ANY} or ${ESCAPE}`,
)
}
}

const parseIndex = function (token) {
return POSITIVE_INTEGER_REGEXP.test(token) ? Number(token) : token
}

const POSITIVE_INTEGER_REGEXP = /^\d+$/u

// Inverse of `parse()`
Expand All @@ -85,22 +106,31 @@ export const serialize = function (tokens) {
}

const serializeToken = function (token, index) {
if (typeof token !== 'string') {
return String(token)
if (index === 0 && token[0] === '') {
return SEPARATOR
}

if (index === 0 && token === '') {
return SEPARATOR
return token.map(serializeTokenPart).join('')
}

const serializeTokenPart = function (tokenPart) {
if (Number.isInteger(tokenPart)) {
return String(tokenPart)
}

return token.replace(UNESCAPED_CHARS_REGEXP, '\\$&')
if (isPlainObj(tokenPart)) {
return ANY
}

return tokenPart.replace(UNESCAPED_CHARS_REGEXP, '\\$&')
}

export const isParent = function (parentQuery, childQuery) {
return childQuery.startsWith(`${parentQuery}${SEPARATOR}`)
}

const ESCAPE = '\\'
export const ANY = '*'
export const SEPARATOR = '.'
const UNESCAPED_CHARS_REGEXP = /[\\.]/gu
export const ANY = '*'
const ANY_TYPE = 'any'
const UNESCAPED_CHARS_REGEXP = /[\\.*]/gu

0 comments on commit 0bf4ac7

Please sign in to comment.