Skip to content

Commit

Permalink
✨ Parse list notation (#159)
Browse files Browse the repository at this point in the history
* ✅ Update toPath tests for list notation

* ✅ Add tests for unterminated list notation

* 🚧 Parse bare list notation

* ♻ Rewrite bareListNotationParser

* 🚧 Manage incomplete bare list notation

* ✨ List notation parser !

* ♻️ Mutualize some parsers

* 🔥 Remove dead code

* 👌 Review @hgwood

* 👌 review @hgwood, extract list props with a generator

* 👌 review @frinyvonnick
  • Loading branch information
nlepage committed Dec 12, 2017
1 parent 20a1153 commit 90e27d8
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 22 deletions.
5 changes: 2 additions & 3 deletions packages/immutadot/src/path/consts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export const prop = Symbol('prop')

export const index = Symbol('index')

export const list = Symbol('list')
export const prop = Symbol('prop')
export const slice = Symbol('slice')
53 changes: 36 additions & 17 deletions packages/immutadot/src/path/toPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {

import {
index,
list,
prop,
slice,
} from './consts'
Expand All @@ -27,7 +28,7 @@ import {
* @function
* @param {string} str The string
* @param {string} quote The quote to unescape
* @return {string} The unescaped string
* @returns {string} The unescaped string
* @memberof path
* @private
* @since 1.0.0
Expand All @@ -39,7 +40,7 @@ const unescapeQuotes = (str, quote) => str.replace(new RegExp(`\\\\${quote}`, 'g
* @function
* @param {string} str The string to convert
* @param {number?} defaultValue The default value if <code>str</code> is empty
* @return {number} <code>undefined</code> if <code>str</code> is empty, otherwise an int (may be NaN)
* @returns {number} <code>undefined</code> if <code>str</code> is empty, otherwise an int (may be NaN)
* @memberof path
* @private
* @since 1.0.0
Expand All @@ -51,7 +52,7 @@ const toSliceIndex = (str, defaultValue) => str === '' ? defaultValue : Number(s
* @function
* @memberof path
* @param {*} arg The value to test
* @return {boolean} True if <code>arg</code> is a valid slice index once converted to a number, false otherwise.
* @returns {boolean} True if <code>arg</code> is a valid slice index once converted to a number, false otherwise.
* @private
* @since 1.0.0
*/
Expand All @@ -64,7 +65,7 @@ const isSliceIndexString = arg => isSliceIndex(arg ? Number(arg) : undefined)
* - Otherwise, calls <code>fn</code> with the string representation of its argument
* @function
* @param {function} fn The function to wrap
* @return {function} The wrapper function
* @returns {function} The wrapper function
* @memberof path
* @private
* @since 1.0.0
Expand Down Expand Up @@ -108,14 +109,33 @@ const sliceNotationParser = map(
([sliceStart, sliceEnd, rest]) => [[slice, [toSliceIndex(sliceStart, 0), toSliceIndex(sliceEnd)]], ...stringToPath(rest)],
)

const pathSegmentEndedByDotParser = map(
regexp(/^([^.[]*?)\.(.*)$/),
([beforeDot, afterDot]) => [[prop, beforeDot], ...stringToPath(afterDot)],
const listPropRegexp = /^,?((?!["'])([^,]*)|(["'])(.*?[^\\])\3)(.*)/
function* extractListProps(rawProps) {
if (rawProps.startsWith(',')) yield ''
let remProps = rawProps
while (remProps !== '') {
const [, , bareProp, , quotedProp, rest] = listPropRegexp.exec(remProps)
yield bareProp === undefined ? quotedProp : bareProp
remProps = rest
}
}

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)]
},
)

const incompleteListNotationParser = map(
regexp(/^(\{[^.[{]*)(.*)$/),
([beforeNewSegment, rest]) => [[prop, beforeNewSegment], ...stringToPath(rest)],
)

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

const applyParsers = race([
Expand All @@ -125,16 +145,16 @@ const applyParsers = race([
sliceNotationParser,
bareBracketNotationParser,
incompleteBareBracketNotationParser,
pathSegmentEndedByDotParser,
pathSegmentEndedByBracketParser,
str => [[prop, str]],
listNotationParser,
incompleteListNotationParser,
pathSegmentEndedByNewSegment,
])

/**
* Converts <code>arg</code> to a path represented as an array of keys.
* @function
* @param {*} arg The value to convert
* @return {Array<string|number|Array>} The path represented as an array of keys
* @returns {Array<string|number|Array>} The path represented as an array of keys
* @memberof path
* @private
* @since 1.0.0
Expand All @@ -152,7 +172,7 @@ const cache = new Map()
* The cache has a maximum size of 1000, when overflowing the cache is cleared.
* @function
* @param {string} str The string to convert
* @return {Array<string|number|Array>} The path represented as an array of keys
* @returns {Array<string|number|Array>} The path represented as an array of keys
* @memberof path
* @private
* @since 1.0.0
Expand All @@ -172,8 +192,7 @@ const memoizedStringToPath = str => {
* Converts <code>arg</code> to a path represented as an array of keys.<br />
* <code>arg</code> may be a string, in which case it will be parsed.<br />
* It may also be an Array, in which case a copy of the array with values converted to path keys will be returned.<br />
* If <code>arg</code> is neither a string nor an Array, its string representation will be parsed.<br />
* This function is failsafe, it will never throw an error.
* If <code>arg</code> is neither a string nor an Array, its string representation will be parsed.
* @function
* @param {string|Array|*} arg The value to convert
* @returns {Array<Array<Symbol,...*>>} The path represented as an array of keys
Expand Down
20 changes: 18 additions & 2 deletions packages/immutadot/src/path/toPath.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-env jest */
import {
index,
list,
prop,
slice,
} from './consts'
Expand Down Expand Up @@ -39,9 +40,22 @@ describe('ToPath', () => {
expect(toPath('[1:2:3][1:a][1:2')).toEqual([[prop, '1:2:3'], [prop, '1:a'], [prop, '1:2']])
})

it('should convert list notation path', () => {
expect(toPath('{abc,defg}.{123,4567,89}.{foo}')).toEqual([[list, ['abc', 'defg']], [list, ['123', '4567', '89']], [prop, 'foo']])
expect(toPath('{"abc,defg",foo}.{\'123,4567,89\'}')).toEqual([[list, ['abc,defg', 'foo']], [prop, '123,4567,89']])
expect(toPath('{,1,2,3}')).toEqual([[list, ['', '1', '2', '3']]])
// Unterminated list notation should give a prop
expect(toPath('abc.{')).toEqual([[prop, 'abc'], [prop, '{']])
expect(toPath('abc.{"')).toEqual([[prop, 'abc'], [prop, '{"']])
expect(toPath('abc.{a,b,c')).toEqual([[prop, 'abc'], [prop, '{a,b,c']])
expect(toPath('{abc,defg[0].foo{bar')).toEqual([[prop, '{abc,defg'], [index, 0], [prop, 'foo'], [prop, '{bar']])
// Unterminated quoted list notation should run to end of path
expect(toPath('{abc,"defg[0]}.foo{\'bar')).toEqual([[prop, '{abc,"defg'], [index, 0], [prop, '}'], [prop, 'foo'], [prop, '{\'bar']])
})

it('should convert mixed path', () => {
expect(toPath('a[0]["b.c"].666[1:]')).toEqual([[prop, 'a'], [index, 0], [prop, 'b.c'], [prop, '666'], [slice, [1, undefined]]])
expect(toPath('a.[0].["b.c"]666[1:2:3]')).toEqual([[prop, 'a'], [index, 0], [prop, 'b.c'], [prop, '666'], [prop, '1:2:3']])
expect(toPath('a[0]["b.c"].666[1:].{1a,2b,3c}')).toEqual([[prop, 'a'], [index, 0], [prop, 'b.c'], [prop, '666'], [slice, [1, undefined]], [list, ['1a', '2b', '3c']]])
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', () => {
Expand All @@ -51,12 +65,14 @@ describe('ToPath', () => {
[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']],
])
})

Expand Down

0 comments on commit 90e27d8

Please sign in to comment.