Skip to content

Commit

Permalink
♻️ Make path steps type more formal (#156)
Browse files Browse the repository at this point in the history
* 🚧 Add symbols prop, index and slice

* ✅ Update toPath tests

* 🚚 Rename path.utils.js to utils.js

* ♻️ Formalize types in path

* ♻️ Formalize types in apply

* ♻️ Formalize types in get

* ♻️ Allow only prop and index types in get path

* ✅ Update pathAlreadyApplied tests

* ♻️ Formalize types in pathAlreadyApplied

* ♻️ allowingArrays won't map props anymore

* ♻️ Formalize prop types in protect

* 💡 Update toPath and unsafeToPath doc

* ♻️ Remove unnecessary undefined
  • Loading branch information
frinyvonnick authored and nlepage committed Dec 7, 2017
1 parent d620d6c commit 3067ed3
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 199 deletions.
11 changes: 9 additions & 2 deletions packages/immutadot/src/core/get.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
index,
prop,
} from 'path/consts'
import { isNil } from 'util/lang'
import { unsafeToPath } from 'path/toPath'

Expand All @@ -16,8 +20,11 @@ export function get(obj, path, defaultValue) {
function walkPath(curObj, remPath) {
if (remPath.length === 0) return curObj === undefined ? defaultValue : curObj
if (isNil(curObj)) return defaultValue
const [prop, ...pathRest] = remPath
const [[, prop], ...pathRest] = remPath
return walkPath(curObj[prop], pathRest)
}
return walkPath(obj, unsafeToPath(path))
const parsedPath = unsafeToPath(path)
if (parsedPath.some(([propType]) => propType !== prop && propType !== index))
throw TypeError('get supports only properties and array indexes in path')
return walkPath(obj, parsedPath)
}
4 changes: 4 additions & 0 deletions packages/immutadot/src/core/get.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ describe('Get', () => {
expect(get(obj, 'nested3.val', 'defaultValue')).toBe('defaultValue')
expect(get(obj, 'nested2.arr[1].val', 'defaultValue')).toBe('defaultValue')
})

it('should throw an error if path includes something else than props and indexes', () => {
expect(() => get({}, 'foo[1:2]')).toThrowError('get supports only properties and array indexes in path')
})
})
32 changes: 18 additions & 14 deletions packages/immutadot/src/path/apply.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
getSliceBounds,
isIndex,
isSlice,
pathAlreadyApplied,
} from './path.utils'
} from './utils'

import {
index,
slice,
} from './consts'

import {
isNil,
Expand Down Expand Up @@ -35,15 +38,15 @@ const copy = (value, asArray) => {
* Makes a copy of <code>value</code> if necessary.
* @function
* @param {*} value The value to make a copy of
* @param {string} prop The accessed property in <code>value</code>
* @param {string} propType The type of the accessed property in <code>value</code>
* @param {boolean} doCopy Whether to make a copy or not
* @returns {Object|Array} A copy of value, or not ;)
* @private
* @since 1.0.0
*/
const copyIfNecessary = (value, prop, doCopy) => {
const copyIfNecessary = (value, propType, doCopy) => {
if (!doCopy) return value
return copy(value, isIndex(prop))
return copy(value, propType === index)
}

/**
Expand Down Expand Up @@ -88,36 +91,37 @@ const apply = operation => {
const applier = (obj, appliedPaths = []) => {
const walkPath = (curObj, curPath, remPath, isCopy = false) => {
const [prop, ...pathRest] = remPath
const [propType, propValue] = prop

if (isSlice(prop)) {
const [start, end] = getSliceBounds(prop, length(curObj))
if (propType === slice) {
const [start, end] = getSliceBounds(propValue, length(curObj))

const newArr = copy(curObj, true)
let noop = true

for (let i = start; i < end; i++) {
const [iNoop] = walkPath(newArr, curPath, [i, ...pathRest], true)
const [iNoop] = walkPath(newArr, curPath, [[index, i], ...pathRest], true)
noop = noop && iNoop
}

if (noop) return [true, curObj]
return [false, newArr]
}

const value = isNil(curObj) ? undefined : curObj[prop]
const value = isNil(curObj) ? undefined : curObj[propValue]
const doCopy = !isCopy && !pathAlreadyApplied(curPath, appliedPaths)

if (remPath.length === 1) {
const newObj = copyIfNecessary(curObj, prop, doCopy)
operation(newObj, prop, value, ...args)
const newObj = copyIfNecessary(curObj, propType, doCopy)
operation(newObj, propValue, value, ...args)
return [false, newObj]
}

const [noop, newValue] = walkPath(value, [...curPath, prop], pathRest)
if (noop) return [true, curObj]

const newObj = copyIfNecessary(curObj, prop, doCopy)
newObj[prop] = newValue
const newObj = copyIfNecessary(curObj, propType, doCopy)
newObj[propValue] = newValue
return [false, newObj]
}

Expand Down
5 changes: 5 additions & 0 deletions packages/immutadot/src/path/consts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const prop = Symbol('prop')

export const index = Symbol('index')

export const slice = Symbol('slice')
96 changes: 0 additions & 96 deletions packages/immutadot/src/path/path.utils.spec.js

This file was deleted.

52 changes: 20 additions & 32 deletions packages/immutadot/src/path/toPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,20 @@ import {
regexp,
} from './parser.utils'

import {
index,
prop,
slice,
} from './consts'

import {
isNil,
isSymbol,
toString,
} from 'util/lang'

import {
isIndex,
} from './path.utils'

/**
* Converts a value to a valid path key.<br />
* Returns <code>arg</code> if arg is a positive integer or a Symbol, <code>toString(arg)</code> otherwise.
* @function
* @param {*} arg The value to convert
* @return {string} A valid path key
* @memberof path
* @private
* @since 1.0.0
*/
const toKey = arg => {
if (isIndex(arg)) return arg
if (isSymbol(arg)) return arg
if (Array.isArray(arg) && arg.length === 2 && isSliceIndex(arg[0]) && isSliceIndex(arg[0])) return arg
return toString(arg)
}
} from './utils'

/**
* Strip slashes preceding occurences of <code>quote</code> from <code>str</code><br />
Expand Down Expand Up @@ -91,52 +79,52 @@ const isSliceIndexString = arg => isSliceIndex(arg ? Number(arg) : undefined)
* @since 1.0.0
*/
const allowingArrays = fn => arg => {
if (Array.isArray(arg)) return arg.map(toKey)
if (Array.isArray(arg)) return arg
return fn(arg)
}

const emptyStringParser = str => str.length === 0 ? [] : null

const quotedBracketNotationParser = map(
regexp(/^\[(['"])(.*?[^\\])\1\]?\.?(.*)$/),
([quote, property, rest]) => [unescapeQuotes(property, quote), ...stringToPath(rest)],
([quote, property, rest]) => [[prop, unescapeQuotes(property, quote)], ...stringToPath(rest)],
)

const incompleteQuotedBracketNotationParser = map(
regexp(/^\[["'](.*)$/),
([rest]) => rest ? [rest] : [],
([rest]) => rest ? [[prop, rest]] : [],
)

const bareBracketNotationParser = map(
regexp(/^\[([^\]]*)\]\.?(.*)$/),
([property, rest]) => {
return isIndex(Number(property))
? [Number(property), ...stringToPath(rest)]
: [property, ...stringToPath(rest)]
? [[index, Number(property)], ...stringToPath(rest)]
: [[prop, property], ...stringToPath(rest)]
},
)

const incompleteBareBracketNotationParser = map(
regexp(/^\[(.*)$/),
([rest]) => rest ? [rest] : [],
([rest]) => rest ? [[prop, rest]] : [],
)

const sliceNotationParser = map(
filter(
regexp(/^\[([^:\]]*):([^:\]]*)\]\.?(.*)$/),
([sliceStart, sliceEnd]) => isSliceIndexString(sliceStart) && isSliceIndexString(sliceEnd),
),
([sliceStart, sliceEnd, rest]) => [[toSliceIndex(sliceStart), toSliceIndex(sliceEnd)], ...stringToPath(rest)],
([sliceStart, sliceEnd, rest]) => [[slice, [toSliceIndex(sliceStart), toSliceIndex(sliceEnd)]], ...stringToPath(rest)],
)

const pathSegmentEndedByDotParser = map(
regexp(/^([^.[]*?)\.(.*)$/),
([beforeDot, afterDot]) => [beforeDot, ...stringToPath(afterDot)],
([beforeDot, afterDot]) => [[prop, beforeDot], ...stringToPath(afterDot)],
)

const pathSegmentEndedByBracketParser = map(
regexp(/^([^.[]*?)(\[.*)$/),
([beforeBracket, atBracket]) => [beforeBracket, ...stringToPath(atBracket)],
([beforeBracket, atBracket]) => [[prop, beforeBracket], ...stringToPath(atBracket)],
)

const applyParsers = race([
Expand All @@ -148,7 +136,7 @@ const applyParsers = race([
incompleteBareBracketNotationParser,
pathSegmentEndedByDotParser,
pathSegmentEndedByBracketParser,
str => [str],
str => [[prop, str]],
])

/**
Expand Down Expand Up @@ -197,18 +185,18 @@ const memoizedStringToPath = str => {
* This function is failsafe, it will never throw an error.
* @function
* @param {string|Array|*} arg The value to convert
* @return {Array<string|number|Array>} The path represented as an array of keys
* @returns {Array<Array<Symbol,...*>>} The path represented as an array of keys
* @memberof path
* @since 1.0.0
* @example toPath('a.b[1]["."][1:-1]') // => ['a', 'b', 1, '.', [1, -1]]
* @example toPath('a.b[1]["."][1:-1]') // => [[prop, 'a'], [prop, 'b'], [index, 1], [prop, '.'], [slice, [1, -1]]]
*/
const toPath = allowingArrays(arg => [...memoizedStringToPath(arg)])

/**
* This method is like {@link core.toPath} except it returns memoized arrays which must not be mutated.
* @function
* @param {string|Array|*} arg The value to convert
* @return {Array<string|number|Array>} The path represented as an array of keys
* @returns {Array<Array<Symbol,...*>>} The path represented as an array of keys
* @memberof path
* @since 1.0.0
* @private
Expand Down

0 comments on commit 3067ed3

Please sign in to comment.