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([])