Skip to content

Commit

Permalink
Add slice type
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Mar 6, 2022
1 parent 32ad777 commit 4bbb9c2
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 17 deletions.
5 changes: 4 additions & 1 deletion src/config/normalize/lib/star_dot_path/parsing/normalize.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { getObjectTokenType } from '../tokens/main.js'

// Normalize a path of tokens
export const normalizePath = function (path) {
return path.map(normalizeToken)
}

const normalizeToken = function (token) {
return token
const tokenType = getObjectTokenType(token)
return tokenType.normalize(token)
}
23 changes: 15 additions & 8 deletions src/config/normalize/lib/star_dot_path/parsing/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ANY,
MINUS,
REGEXP_DELIM,
SLICE,
} from '../tokens/special.js'

import { normalizePath } from './normalize.js'
Expand Down Expand Up @@ -34,6 +35,12 @@ import { isQueryString } from './validate.js'
// - Tokens format: 1 (must be an integer, not a string)
// - Negatives indices can be used to get elements at the end, e.g. -2
// - Including -0 which can be used to append elements
// - Array slices
// - Query format: "0:2"
// - Tokens format: { type: "slice", from: 0, end: 2 }
// - Matches multiple indices of an array
// - Negatives indices like the array indices format
// - `from` defaults to 0 and `to` to -0
// - Wildcard
// - Query format: "*"
// - Tokens format: { type: "any" }
Expand Down Expand Up @@ -100,14 +107,9 @@ const parseQuery = function (query) {

const getInitialState = function (query) {
const index = query[0] === SEPARATOR ? 1 : 0
return {
path: [],
chars: '',
index,
hasAny: false,
hasMinus: false,
hasRegExp: false,
}
const state = { path: [], index }
resetState(state)
return state
}

const addEscapedChar = function (state, query) {
Expand All @@ -120,10 +122,14 @@ const addToken = function (state) {
const token = tokenType.parse(state.chars)
// eslint-disable-next-line fp/no-mutating-methods
state.path.push(token)
resetState(state)
}

const resetState = function (state) {
state.hasAny = false
state.hasMinus = false
state.hasRegExp = false
state.hasSlice = false
state.chars = ''
}

Expand All @@ -135,5 +141,6 @@ const addChar = function (state, char) {
state.hasRegExp = state.hasRegExp || char === REGEXP_DELIM
}

state.hasSlice = state.hasSlice || char === SLICE
state.chars += char
}
1 change: 1 addition & 0 deletions src/config/normalize/lib/star_dot_path/remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const removeValue = function (target, key) {
: removeObjectValue(target, key)
}

// We make sure removing out-of-bound does not increase its length
const removeArrayValue = function (target, key) {
if (target[key] === undefined) {
return target
Expand Down
6 changes: 6 additions & 0 deletions src/config/normalize/lib/star_dot_path/tokens/any.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ Otherwise, please escape it with a "${ESCAPE}".`,
return { type: ANY_TYPE }
}

// Normalize value after parsing or serializing
const normalize = function ({ type }) {
return { type }
}

// When the token is missing a target value, add a default one.
const isDefined = function (value) {
return isRecurseObject(value)
Expand Down Expand Up @@ -70,6 +75,7 @@ export const ANY_TOKEN = {
serialize,
testString,
parse,
normalize,
isDefined,
defaultValue,
getEntries,
Expand Down
16 changes: 13 additions & 3 deletions src/config/normalize/lib/star_dot_path/tokens/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MINUS } from './special.js'
// Check the type of a parsed token.
// Integers specified as string tokens are assumed to be property names, not
// array indices.
const testObject = function (token) {
export const testObject = function (token) {
return Number.isInteger(token)
}

Expand All @@ -15,7 +15,11 @@ const serialize = function (token) {
// Check the type of a serialized token
const testString = function ({ chars, hasMinus }) {
const hasEscapedMinus = chars[0] === MINUS && !hasMinus
return !hasEscapedMinus && INTEGER_REGEXP.test(chars)
return !hasEscapedMinus && isIndexString(chars)
}

export const isIndexString = function (chars) {
return INTEGER_REGEXP.test(chars)
}

const INTEGER_REGEXP = /^-?\d+$/u
Expand All @@ -25,6 +29,11 @@ const parse = function (chars) {
return Number(chars)
}

// Normalize value after parsing or serializing
const normalize = function (token) {
return token
}

// When the token is missing a target value, add a default one.
const isDefined = function (value) {
return Array.isArray(value)
Expand All @@ -45,7 +54,7 @@ const getEntries = function (value, path, token, defined) {
// - Do not error
// - Return an entry with an `undefined` value
// - This allows appending to arrays, e.g. with -0
const getArrayIndex = function (value, token) {
export const getArrayIndex = function (value, token) {
return token > 0 || Object.is(token, +0)
? token
: Math.max(value.length + token, 0)
Expand All @@ -61,6 +70,7 @@ export const ARRAY_TOKEN = {
serialize,
testString,
parse,
normalize,
isDefined,
defaultValue,
getEntries,
Expand Down
22 changes: 18 additions & 4 deletions src/config/normalize/lib/star_dot_path/tokens/escape.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ESCAPE, SEPARATOR, ANY, MINUS, REGEXP_DELIM } from './special.js'
import {
ESCAPE,
SEPARATOR,
ANY,
MINUS,
REGEXP_DELIM,
SLICE,
} from './special.js'

// Parse an escaped character in a query string
export const parseEscapedChar = function (escapedChar) {
Expand All @@ -9,16 +16,23 @@ export const parseEscapedChar = function (escapedChar) {
const validateEscape = function (escapedChar) {
if (!SPECIAL_CHARS.has(escapedChar)) {
throw new Error(
`character "${ESCAPE}" must only be followed by ${SEPARATOR} ${ANY} ${MINUS} ${REGEXP_DELIM} or ${ESCAPE}`,
`character "${ESCAPE}" must only be followed by ${SEPARATOR} ${ANY} ${MINUS} ${REGEXP_DELIM} ${SLICE} or ${ESCAPE}`,
)
}
}

const SPECIAL_CHARS = new Set([ESCAPE, SEPARATOR, ANY, MINUS, REGEXP_DELIM])
const SPECIAL_CHARS = new Set([
ESCAPE,
SEPARATOR,
ANY,
MINUS,
REGEXP_DELIM,
SLICE,
])

// Escape special characters
export const escapeSpecialChars = function (string) {
return string.replace(SPECIAL_CHARS_REGEXP, `${ESCAPE}$&`)
}

const SPECIAL_CHARS_REGEXP = /[\\.*]|^[/-]/gu
const SPECIAL_CHARS_REGEXP = /[\\.*:]|^[/-]/gu
9 changes: 8 additions & 1 deletion src/config/normalize/lib/star_dot_path/tokens/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@ import { ANY_TOKEN } from './any.js'
import { ARRAY_TOKEN } from './array.js'
import { PROP_TOKEN } from './prop.js'
import { REGEXP_TOKEN } from './regexp.js'
import { SLICE_TOKEN } from './slice.js'

// Order is significant as they are tested serially
const TOKEN_TYPES = [ANY_TOKEN, REGEXP_TOKEN, ARRAY_TOKEN, PROP_TOKEN]
const TOKEN_TYPES = [
ANY_TOKEN,
REGEXP_TOKEN,
SLICE_TOKEN,
ARRAY_TOKEN,
PROP_TOKEN,
]

// Retrieve the type of a given token parsed object
export const getObjectTokenType = function (token) {
Expand Down
6 changes: 6 additions & 0 deletions src/config/normalize/lib/star_dot_path/tokens/prop.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const parse = function (chars) {
return chars
}

// Normalize value after parsing or serializing
const normalize = function (token) {
return token
}

// When the token is missing a target value, add a default one.
const isDefined = function (value) {
return isRecurseObject(value)
Expand Down Expand Up @@ -55,6 +60,7 @@ export const PROP_TOKEN = {
serialize,
testString,
parse,
normalize,
isDefined,
defaultValue,
getEntries,
Expand Down
6 changes: 6 additions & 0 deletions src/config/normalize/lib/star_dot_path/tokens/regexp.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ const parse = function (chars) {
return new RegExp(regExpString, regExpFlags)
}

// Normalize value after parsing or serializing
const normalize = function (token) {
return token
}

// When the token is missing a target value, add a default one.
const isDefined = function (value) {
return isRecurseObject(value)
Expand Down Expand Up @@ -64,6 +69,7 @@ export const REGEXP_TOKEN = {
serialize,
testString,
parse,
normalize,
isDefined,
defaultValue,
getEntries,
Expand Down
92 changes: 92 additions & 0 deletions src/config/normalize/lib/star_dot_path/tokens/slice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import isPlainObj from 'is-plain-obj'

import { ARRAY_TOKEN, getArrayIndex, isIndexString } from './array.js'
import { SLICE } from './special.js'

// Check the type of a parsed token.
const testObject = function (token) {
return (
isPlainObj(token) &&
token.type === SLICE_TYPE &&
isEdge(token.from) &&
isEdge(token.to)
)
}

const isEdge = function (edge) {
return edge === undefined || ARRAY_TOKEN.testObject(edge)
}

// Serialize a token to a string
const serialize = function ({ from, to }) {
return `${serializeEdge(from)}${SLICE}${serializeEdge(to)}`
}

const serializeEdge = function (edge) {
return edge === undefined ? DEFAULT_EDGE_STRING : ARRAY_TOKEN.serialize(edge)
}

// Check the type of a serialized token
const testString = function ({ chars, hasSlice }) {
return hasSlice && chars.split(SLICE).every(isEdgeString)
}

const isEdgeString = function (chars) {
return chars === DEFAULT_EDGE_STRING || isIndexString(chars)
}

// Parse a string into a token
const parse = function (chars) {
const [from, to] = chars.split(SLICE).map(parseEdge)
return { type: SLICE_TYPE, from, to }
}

const parseEdge = function (chars) {
return chars === DEFAULT_EDGE_STRING ? undefined : ARRAY_TOKEN.parse(chars)
}

const DEFAULT_EDGE_STRING = ''
const SLICE_TYPE = 'slice'

// Normalize value after parsing or serializing
const normalize = function ({ type, from = 0, to }) {
const toA = Object.is(to, -0) ? undefined : to
return { type, from, to: toA }
}

// When the token is missing a target value, add a default one.
const isDefined = function (value) {
return Array.isArray(value)
}

// Default value when token is missing
const defaultValue = []

// Use the token to list entries against a target value.
// eslint-disable-next-line max-params
const getEntries = function (value, path, { from, to }, defined) {
const fromIndex = getArrayIndex(value, from)
const toIndex = Math.max(getArrayIndex(value, to), fromIndex)
return new Array(toIndex - fromIndex).fill().map((_, index) => ({
value: value[index + fromIndex],
path: [...path, index + fromIndex],
defined,
}))
}

// Check if two tokens are the same
const equals = function (tokenA, tokenB) {
return Object.is(tokenA.from, tokenB.from) && Object.is(tokenA.to, tokenB.to)
}

export const SLICE_TOKEN = {
testObject,
serialize,
testString,
parse,
normalize,
isDefined,
defaultValue,
getEntries,
equals,
}
1 change: 1 addition & 0 deletions src/config/normalize/lib/star_dot_path/tokens/special.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const SEPARATOR = '.'
export const ANY = '*'
export const MINUS = '-'
export const REGEXP_DELIM = '/'
export const SLICE = ':'

0 comments on commit 4bbb9c2

Please sign in to comment.