diff --git a/packages/immutadot/src/core/get.js b/packages/immutadot/src/core/get.js index 899d3168..fd02eadf 100644 --- a/packages/immutadot/src/core/get.js +++ b/packages/immutadot/src/core/get.js @@ -3,7 +3,7 @@ import { prop, } from 'path/consts' import { isNil } from 'util/lang' -import { unsafeToPath } from 'path/toPath' +import { toPath } from 'path/toPath' /** * Gets the value at path of obj. @@ -23,7 +23,7 @@ function get(obj, path, defaultValue) { const [[, prop], ...pathRest] = remPath return walkPath(curObj[prop], pathRest) } - const parsedPath = unsafeToPath(path) + const parsedPath = toPath(path) if (parsedPath.some(([propType]) => propType !== prop && propType !== index)) throw TypeError('get supports only properties and array indexes in path') return walkPath(obj, parsedPath) diff --git a/packages/immutadot/src/path/apply.js b/packages/immutadot/src/path/apply.js index d52a0e41..5c33c106 100644 --- a/packages/immutadot/src/path/apply.js +++ b/packages/immutadot/src/path/apply.js @@ -17,7 +17,7 @@ import { length, } from 'util/lang' -import { unsafeToPath } from './toPath' +import { toPath } from './toPath' /** * Makes a copy of value. @@ -88,7 +88,7 @@ const copyIfNecessary = (value, propType, doCopy) => { */ const apply = operation => { const curried = (pPath, ...args) => { - const path = unsafeToPath(pPath) + const path = toPath(pPath) if (path.length === 0) throw new TypeError('path should not be empty') diff --git a/packages/immutadot/src/path/index.js b/packages/immutadot/src/path/index.js deleted file mode 100644 index 19fd7e9e..00000000 --- a/packages/immutadot/src/path/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/** -* Path functions. -* @namespace path -* @since 1.0.0 -*/ - -export { toPath } from './toPath' diff --git a/packages/immutadot/src/path/toPath.js b/packages/immutadot/src/path/toPath.js index 333b0955..09422032 100644 --- a/packages/immutadot/src/path/toPath.js +++ b/packages/immutadot/src/path/toPath.js @@ -59,47 +59,30 @@ const toSliceIndex = (str, defaultValue) => str === '' ? defaultValue : Number(s */ const isSliceIndexString = arg => isSliceIndex(arg ? Number(arg) : undefined) -/** - * Wraps fn allowing to call it with an array instead of a string.
- * The returned function behaviour is :
- * - If called with an array, returns a copy of the array with values converted to path keys
- * - Otherwise, calls fn with the string representation of its argument - * @function - * @param {function} fn The function to wrap - * @returns {function} The wrapper function - * @memberof path - * @private - * @since 1.0.0 - */ -const allowingArrays = fn => arg => { - if (Array.isArray(arg)) return arg - return fn(arg) -} - const emptyStringParser = str => str.length === 0 ? [] : null const quotedBracketNotationParser = map( regexp(/^\[(['"])(.*?[^\\])\1\]?\.?(.*)$/), - ([quote, property, rest]) => [[prop, unescapeQuotes(property, quote)], ...stringToPath(rest)], + ([quote, property, rest]) => [[prop, unescapeQuotes(property, quote)], ...applyParsers(rest)], ) const incompleteQuotedBracketNotationParser = map( regexp(/^(\[["'][^.[{]*)\.?(.*)$/), - ([beforeNewSegment, rest]) => [[prop, beforeNewSegment], ...stringToPath(rest)], + ([beforeNewSegment, rest]) => [[prop, beforeNewSegment], ...applyParsers(rest)], ) const bareBracketNotationParser = map( regexp(/^\[([^\]]*)\]\.?(.*)$/), ([property, rest]) => { return isIndex(Number(property)) - ? [[index, Number(property)], ...stringToPath(rest)] - : [[prop, property], ...stringToPath(rest)] + ? [[index, Number(property)], ...applyParsers(rest)] + : [[prop, property], ...applyParsers(rest)] }, ) const incompleteBareBracketNotationParser = map( regexp(/^(\[[^.[{]*)\.?(.*)$/), - ([beforeNewSegment, rest]) => [[prop, beforeNewSegment], ...stringToPath(rest)], + ([beforeNewSegment, rest]) => [[prop, beforeNewSegment], ...applyParsers(rest)], ) const sliceNotationParser = map( @@ -107,12 +90,12 @@ const sliceNotationParser = map( regexp(/^\[([^:\]]*):([^:\]]*)\]\.?(.*)$/), ([sliceStart, sliceEnd]) => isSliceIndexString(sliceStart) && isSliceIndexString(sliceEnd), ), - ([sliceStart, sliceEnd, rest]) => [[slice, [toSliceIndex(sliceStart, 0), toSliceIndex(sliceEnd)]], ...stringToPath(rest)], + ([sliceStart, sliceEnd, rest]) => [[slice, [toSliceIndex(sliceStart, 0), toSliceIndex(sliceEnd)]], ...applyParsers(rest)], ) const listWildCardParser = map( regexp(/^{\*}\.?(.*)$/), - ([rest]) => [[allProps], ...stringToPath(rest)], + ([rest]) => [[allProps], ...applyParsers(rest)], ) const listPropRegexp = /^,?((?!["'])([^,]*)|(["'])(.*?[^\\])\3)(.*)/ @@ -130,18 +113,18 @@ const listNotationParser = map( regexp(/^\{(((?!["'])[^,}]*|(["']).*?[^\\]\2)(,((?!["'])[^,}]*|(["']).*?[^\\]\6))*)\}\.?(.*)$/), ([rawProps, , , , , , rest]) => { const props = [...extractListProps(rawProps)] - return props.length === 1 ? [[prop, props[0]], ...stringToPath(rest)] : [[list, props], ...stringToPath(rest)] + return props.length === 1 ? [[prop, props[0]], ...applyParsers(rest)] : [[list, props], ...applyParsers(rest)] }, ) const incompleteListNotationParser = map( regexp(/^(\{[^.[{]*)\.?(.*)$/), - ([beforeNewSegment, rest]) => [[prop, beforeNewSegment], ...stringToPath(rest)], + ([beforeNewSegment, rest]) => [[prop, beforeNewSegment], ...applyParsers(rest)], ) const pathSegmentEndedByNewSegment = map( regexp(/^([^.[{]*)\.?([[{]?.*)$/), - ([beforeNewSegment, rest]) => [[prop, beforeNewSegment], ...stringToPath(rest)], + ([beforeNewSegment, rest]) => [[prop, beforeNewSegment], ...applyParsers(rest)], ) const applyParsers = race([ @@ -157,29 +140,23 @@ const applyParsers = race([ pathSegmentEndedByNewSegment, ]) -/** - * Converts arg to a path represented as an array of keys. - * @function - * @param {*} arg The value to convert - * @returns {Array} The path represented as an array of keys - * @memberof path - * @private - * @since 1.0.0 - */ -const stringToPath = arg => { - if (isNil(arg)) return [] - return applyParsers(toString(arg)) -} - const MAX_CACHE_SIZE = 1000 const cache = new Map() +const stringToPath = pStr => { + const str = pStr.startsWith('.') ? pStr.substring(1) : pStr + + const path = applyParsers(str) + + return pStr.endsWith('.') ? [...path, [prop, '']] : path +} + /** - * Memoized version of {@link core.stringToPath}.
+ * Memoized version of {@link path.stringToPath}.
* The cache has a maximum size of 1000, when overflowing the cache is cleared. * @function * @param {string} str The string to convert - * @returns {Array} The path represented as an array of keys + * @returns {Array>} The path represented as an array of keys * @memberof path * @private * @since 1.0.0 @@ -202,22 +179,16 @@ const memoizedStringToPath = str => { * If arg is neither a string nor an Array, its string representation will be parsed. * @function * @param {string|Array|*} arg The value to convert - * @returns {Array>} The path represented as an array of keys + * @returns {Array>} The path represented as an array of keys * @memberof path * @since 1.0.0 * @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 - * @returns {Array>} The path represented as an array of keys - * @memberof path - * @since 1.0.0 * @private */ -const unsafeToPath = allowingArrays(memoizedStringToPath) +const toPath = arg => { + if (isNil(arg)) return [] + + return memoizedStringToPath(toString(arg)) +} -export { toPath, unsafeToPath } +export { toPath } diff --git a/packages/immutadot/src/path/toPath.spec.js b/packages/immutadot/src/path/toPath.spec.js index 9b06b52f..771d867a 100644 --- a/packages/immutadot/src/path/toPath.spec.js +++ b/packages/immutadot/src/path/toPath.spec.js @@ -7,14 +7,18 @@ import { slice, } from './consts' -import { toPath } from 'path' +import { toPath } from './toPath' describe('path.toPath', () => { it('should convert basic path', () => { expect(toPath('a.22.ccc')).toEqual([[prop, 'a'], [prop, '22'], [prop, 'ccc']]) + // Leading dot should be discarded + expect(toPath('.a')).toEqual([[prop, 'a']]) // Empty properties should be kept expect(toPath('.')).toEqual([[prop, '']]) + expect(toPath('..prop')).toEqual([[prop, ''], [prop, 'prop']]) + expect(toPath('.a.')).toEqual([[prop, 'a'], [prop, '']]) expect(toPath('..')).toEqual([[prop, ''], [prop, '']]) // If no separators, path should be interpreted as one property expect(toPath('\']"\\')).toEqual([[prop, '\']"\\']]) @@ -64,24 +68,6 @@ describe('path.toPath', () => { expect(toPath('a.[0].["b.c"]666[1:2:3]{1a}{"2b",\'3c\'}')).toEqual([[prop, 'a'], [index, 0], [prop, 'b.c'], [prop, '666'], [prop, '1:2:3'], [prop, '1a'], [list, ['2b', '3c']]]) }) - it('should not convert array path', () => { - expect(toPath([ - [index, 666], - [prop, Symbol.for('🍺')], - [prop, 'test'], - [slice, [1, undefined]], - [slice, [0, -2]], - [list, ['1a', '2b', '3c']], - ])).toEqual([ - [index, 666], - [prop, Symbol.for('🍺')], - [prop, 'test'], - [slice, [1, undefined]], - [slice, [0, -2]], - [list, ['1a', '2b', '3c']], - ]) - }) - it('should give empty path for nil values', () => { expect(toPath(null)).toEqual([]) expect(toPath(undefined)).toEqual([])